More libs added.

This commit is contained in:
Scott Duensing 2026-07-03 02:13:23 -05:00
parent 82c040395d
commit 144c3f1ca2
25 changed files with 5108 additions and 3 deletions

View file

@ -136,7 +136,7 @@ ENETOBJ = $(foreach n,$(ENETNAMES),obj/enet_$(n).o)
BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \
bin/testEngineLua bin/testEngineMyBasic bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \
bin/testEngineBerry bin/testEngineS7 bin/testEngineWren bin/testLoad bin/testDb bin/testNet bin/testTask bin/testExport bin/embed
bin/testEngineBerry bin/testEngineS7 bin/testEngineWren bin/testLoad bin/testDb bin/testNet bin/testTask bin/testExport bin/testJson bin/testTime bin/testFs bin/testCrypto bin/testKv bin/testTimer bin/testPubsub bin/testHttp bin/embed
all: $(BINS)
@ -148,10 +148,15 @@ $(STRICTOBJ): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) -c -o $@ $<
# strict C, threaded
THREADOBJ = obj/context.o obj/mybasicEngine.o obj/testActor.o obj/testEngineLua.o obj/testEngineSquirrel.o obj/testEngineJs.o obj/testEngineMyBasic.o obj/testEngineBerry.o obj/testEngineS7.o obj/testEngineWren.o obj/calogHandle.o obj/testDb.o obj/testNet.o obj/testTask.o obj/calogExport.o obj/testExport.o
THREADOBJ = obj/context.o obj/mybasicEngine.o obj/testActor.o obj/testEngineLua.o obj/testEngineSquirrel.o obj/testEngineJs.o obj/testEngineMyBasic.o obj/testEngineBerry.o obj/testEngineS7.o obj/testEngineWren.o obj/calogHandle.o obj/testDb.o obj/testNet.o obj/testTask.o obj/calogExport.o obj/testExport.o obj/calogJson.o obj/testJson.o obj/calogFs.o obj/testFs.o obj/calogTime.o obj/testTime.o obj/calogKv.o obj/testKv.o obj/testCrypto.o obj/calogTimer.o obj/testTimer.o obj/calogPubsub.o obj/testPubsub.o obj/testHttp.o
$(THREADOBJ): obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) -pthread -c -o $@ $<
# calogCrypto and calogHttp need the vendored OpenSSL headers, which are outside $(INC).
OSSLINC = -Ivendor/openssl/include
obj/calogCrypto.o obj/calogHttp.o: obj/%.o: %.c | obj
$(CC) $(COREFLAGS) $(INC) $(OSSLINC) -pthread -c -o $@ $<
# relaxed C + Lua headers
LUAADP = obj/luaAdapter.o obj/testLua.o
$(LUAADP): obj/%.o: %.c | obj
@ -402,13 +407,37 @@ bin/testTask: obj/testTask.o $(TASKADP) obj/calogHandle.o lib/libcalog.a lib/lib
bin/testExport: obj/testExport.o obj/calogExport.o lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libs7.a lib/libmybasic.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -ldl -lm
bin/testJson: obj/testJson.o obj/calogJson.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testTime: obj/testTime.o obj/calogTime.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testFs: obj/testFs.o obj/calogFs.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testCrypto: obj/testCrypto.o obj/calogCrypto.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(SSLARCH) $(LUALIBS) -ldl
bin/testKv: obj/testKv.o obj/calogKv.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testTimer: obj/testTimer.o obj/calogTimer.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testPubsub: obj/testPubsub.o obj/calogPubsub.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS)
bin/testHttp: obj/testHttp.o obj/calogHttp.o lib/libcalog.a lib/liblua.a | bin
$(CC) $(LDFLAGS) -pthread -o $@ $^ $(SSLARCH) $(LUALIBS) -ldl
obj bin lib:
mkdir -p $@
test: all
./bin/testBroker && ./bin/testLua && ./bin/testMyBasic && ./bin/testPolyglot && \
./bin/testActor && ./bin/testEngineLua && ./bin/testEngineMyBasic && ./bin/testSquirrel && ./bin/testEngineSquirrel && \
./bin/testJs && ./bin/testEngineJs && ./bin/testEngineBerry && ./bin/testEngineS7 && ./bin/testEngineWren && ./bin/testLoad && ./bin/testDb && ./bin/testNet && ./bin/testTask && ./bin/testExport
./bin/testJs && ./bin/testEngineJs && ./bin/testEngineBerry && ./bin/testEngineS7 && ./bin/testEngineWren && ./bin/testLoad && ./bin/testDb && ./bin/testNet && ./bin/testTask && ./bin/testExport && ./bin/testJson && ./bin/testTime && ./bin/testFs && ./bin/testCrypto && ./bin/testKv && ./bin/testTimer && ./bin/testPubsub && ./bin/testHttp
# ThreadSanitizer build of the actor core and the Lua engine path (cannot combine
# with ASan). Recompiled from source under TSan; the vendored Lua objects are
@ -488,6 +517,23 @@ tsanwren: | bin obj
setarch -R ./bin/testEngineWrenTsan
rm -f obj/wren.tsan.o
# ThreadSanitizer build of the concurrent libraries (timer's background thread, pubsub's
# callback fan-out, kv's shared store), each over a Lua context so callbacks marshal across
# threads. The unsanitized liblua.a links in fine (TSan instruments only the calog code).
tsanlibs: lib/liblua.a | bin
$(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(LUAINC) -o bin/testTimerTsan \
tests/testTimer.c libs/calogTimer.c src/context.c src/value.c src/broker.c src/lua/luaEngine.c src/lua/luaAdapter.c \
lib/liblua.a $(LUALIBS)
setarch -R ./bin/testTimerTsan
$(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(LUAINC) -o bin/testPubsubTsan \
tests/testPubsub.c libs/calogPubsub.c src/context.c src/value.c src/broker.c src/lua/luaEngine.c src/lua/luaAdapter.c \
lib/liblua.a $(LUALIBS)
setarch -R ./bin/testPubsubTsan
$(CC) -std=c11 $(WARN) -g -O1 -fsanitize=thread -pthread $(INC) $(LUAINC) -o bin/testKvTsan \
tests/testKv.c libs/calogKv.c src/context.c src/value.c src/broker.c src/lua/luaEngine.c src/lua/luaAdapter.c \
lib/liblua.a $(LUALIBS)
setarch -R ./bin/testKvTsan
clean:
rm -rf obj bin lib

346
libs/calogCrypto.c Normal file
View file

@ -0,0 +1,346 @@
// calogCrypto.c -- calog cryptography library (see calogCrypto.h). Thin, binary-safe
// bindings over OpenSSL's one-shot primitives (EVP_Digest, HMAC, RAND_bytes,
// EVP_EncodeBlock/EVP_DecodeBlock): SHA-256/SHA-1 hashing, HMAC-SHA-256, random bytes,
// base64/hex codecs, and version-4 UUIDs. No shared state: every native is a pure function
// of its arguments.
#include "calogCrypto.h"
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length);
static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage);
static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHexNibble(unsigned char c);
static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogCryptoRegister(CalogT *calog) {
calogRegisterInline(calog, "hashSha256", cryptoHashSha256Native, NULL);
calogRegisterInline(calog, "hashSha1", cryptoHashSha1Native, NULL);
calogRegisterInline(calog, "hmacSha256", cryptoHmacSha256Native, NULL);
calogRegisterInline(calog, "randomBytes", cryptoRandomBytesNative, NULL);
calogRegisterInline(calog, "base64Encode", cryptoBase64EncodeNative, NULL);
calogRegisterInline(calog, "base64Decode", cryptoBase64DecodeNative, NULL);
calogRegisterInline(calog, "hexEncode", cryptoHexEncodeNative, NULL);
calogRegisterInline(calog, "hexDecode", cryptoHexDecodeNative, NULL);
calogRegisterInline(calog, "uuid", cryptoUuidNative, NULL);
return calogOkE;
}
static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
const unsigned char *in;
unsigned char *out;
int64_t length;
size_t outCap;
int32_t decoded;
int32_t pad;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "base64Decode expects (text)");
}
length = args[0].as.s.length;
in = (const unsigned char *)args[0].as.s.bytes;
// EVP_DecodeBlock ignores trailing whitespace before decoding; trim it here too so the
// decoded length and the '=' padding count are computed from the real base64 content
// (otherwise "YQ==\n" would decode with a spurious trailing NUL).
while (length > 0) {
unsigned char c;
c = in[length - 1];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v') {
length--;
} else {
break;
}
}
if (length == 0) {
return calogValueString(result, "", 0);
}
if ((length % 4) != 0 || length > INT_MAX) {
return calogFail(result, calogErrArgE, "base64Decode: invalid base64 length");
}
outCap = ((size_t)length / 4) * 3;
out = (unsigned char *)malloc(outCap);
if (out == NULL) {
return calogFail(result, calogErrOomE, "base64Decode: out of memory");
}
// EVP_DecodeBlock always emits a multiple of three bytes and does NOT drop the bytes that
// correspond to '=' padding, so trim one output byte per trailing '=' ourselves.
decoded = EVP_DecodeBlock(out, in, (int)length);
if (decoded < 0) {
free(out);
return calogFail(result, calogErrArgE, "base64Decode: invalid base64 data");
}
pad = 0;
if (in[length - 1] == '=') {
pad++;
if (in[length - 2] == '=') {
pad++;
}
}
status = calogValueString(result, (const char *)out, (int64_t)(decoded - pad));
free(out);
if (status != calogOkE) {
return calogFail(result, status, "base64Decode: out of memory");
}
return calogOkE;
}
static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
unsigned char *out;
size_t inLen;
size_t outCap;
int32_t written;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "base64Encode expects (data)");
}
if (args[0].as.s.length > INT_MAX) {
return calogFail(result, calogErrRangeE, "base64Encode: input too large");
}
inLen = (size_t)args[0].as.s.length;
if (inLen == 0) {
return calogValueString(result, "", 0);
}
outCap = ((inLen + 2) / 3) * 4 + 1; // 4 base64 chars per 3 input bytes, plus the NUL EVP writes
out = (unsigned char *)malloc(outCap);
if (out == NULL) {
return calogFail(result, calogErrOomE, "base64Encode: out of memory");
}
written = EVP_EncodeBlock(out, (const unsigned char *)args[0].as.s.bytes, (int)inLen);
status = calogValueString(result, (const char *)out, (int64_t)written);
free(out);
if (status != calogOkE) {
return calogFail(result, status, "base64Encode: out of memory");
}
return calogOkE;
}
static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length) {
static const char digits[] = "0123456789abcdef";
char *hex;
size_t index;
int32_t status;
if (length == 0) {
return calogValueString(result, "", 0);
}
hex = (char *)malloc(length * 2);
if (hex == NULL) {
return calogErrOomE;
}
for (index = 0; index < length; index++) {
unsigned char byte;
byte = bytes[index];
hex[index * 2] = digits[byte >> 4];
hex[index * 2 + 1] = digits[byte & 0x0F];
}
status = calogValueString(result, hex, (int64_t)(length * 2));
free(hex);
return status;
}
static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage) {
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLen;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, usage);
}
digestLen = 0;
if (EVP_Digest(args[0].as.s.bytes, (size_t)args[0].as.s.length, digest, &digestLen, md, NULL) != 1) {
return calogFail(result, calogErrUnsupportedE, "digest computation failed");
}
return cryptoBytesToHex(result, digest, (size_t)digestLen);
}
static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
return cryptoDigestHex(args, argCount, result, EVP_sha1(), "hashSha1 expects (data)");
}
static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
return cryptoDigestHex(args, argCount, result, EVP_sha256(), "hashSha256 expects (data)");
}
static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
const unsigned char *in;
unsigned char *out;
int64_t length;
int64_t outLen;
int64_t index;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "hexDecode expects (hexText)");
}
length = args[0].as.s.length;
if ((length % 2) != 0) {
return calogFail(result, calogErrArgE, "hexDecode: odd-length hex string");
}
if (length == 0) {
return calogValueString(result, "", 0);
}
in = (const unsigned char *)args[0].as.s.bytes;
outLen = length / 2;
out = (unsigned char *)malloc((size_t)outLen);
if (out == NULL) {
return calogFail(result, calogErrOomE, "hexDecode: out of memory");
}
for (index = 0; index < outLen; index++) {
int32_t hi;
int32_t lo;
hi = cryptoHexNibble(in[index * 2]);
lo = cryptoHexNibble(in[index * 2 + 1]);
if (hi < 0 || lo < 0) {
free(out);
return calogFail(result, calogErrArgE, "hexDecode: invalid hex digit");
}
out[index] = (unsigned char)((hi << 4) | lo);
}
status = calogValueString(result, (const char *)out, outLen);
free(out);
if (status != calogOkE) {
return calogFail(result, status, "hexDecode: out of memory");
}
return calogOkE;
}
static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "hexEncode expects (data)");
}
return cryptoBytesToHex(result, (const unsigned char *)args[0].as.s.bytes, (size_t)args[0].as.s.length);
}
static int32_t cryptoHexNibble(unsigned char c) {
if (c >= '0' && c <= '9') {
return (int32_t)(c - '0');
}
if (c >= 'a' && c <= 'f') {
return (int32_t)(c - 'a' + 10);
}
if (c >= 'A' && c <= 'F') {
return (int32_t)(c - 'A' + 10);
}
return -1;
}
static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLen;
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "hmacSha256 expects (key, data)");
}
if (args[0].as.s.length > INT_MAX) {
return calogFail(result, calogErrRangeE, "hmacSha256: key too large");
}
digestLen = 0;
if (HMAC(EVP_sha256(), args[0].as.s.bytes, (int)args[0].as.s.length, (const unsigned char *)args[1].as.s.bytes, (size_t)args[1].as.s.length, digest, &digestLen) == NULL) {
return calogFail(result, calogErrUnsupportedE, "hmacSha256 computation failed");
}
return cryptoBytesToHex(result, digest, (size_t)digestLen);
}
static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
unsigned char *buf;
int64_t count;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "randomBytes expects (count)");
}
count = args[0].as.i;
if (count < 0 || count > INT_MAX) {
return calogFail(result, calogErrRangeE, "randomBytes: count out of range");
}
if (count == 0) {
return calogValueString(result, "", 0);
}
buf = (unsigned char *)malloc((size_t)count);
if (buf == NULL) {
return calogFail(result, calogErrOomE, "randomBytes: out of memory");
}
if (RAND_bytes(buf, (int)count) != 1) {
free(buf);
return calogFail(result, calogErrUnsupportedE, "randomBytes: RAND_bytes failed");
}
status = calogValueString(result, (const char *)buf, count);
free(buf);
if (status != calogOkE) {
return calogFail(result, status, "randomBytes: out of memory");
}
return calogOkE;
}
static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
static const char digits[] = "0123456789abcdef";
unsigned char bytes[16];
char text[37];
int32_t bi;
int32_t ti;
(void)args;
(void)userData;
calogValueNil(result);
if (argCount != 0) {
return calogFail(result, calogErrArgE, "uuid expects no arguments");
}
if (RAND_bytes(bytes, (int)sizeof(bytes)) != 1) {
return calogFail(result, calogErrUnsupportedE, "uuid: RAND_bytes failed");
}
bytes[6] = (unsigned char)((bytes[6] & 0x0F) | 0x40); // version 4
bytes[8] = (unsigned char)((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant
ti = 0;
for (bi = 0; bi < 16; bi++) {
if (bi == 4 || bi == 6 || bi == 8 || bi == 10) {
text[ti++] = '-';
}
text[ti++] = digits[bytes[bi] >> 4];
text[ti++] = digits[bytes[bi] & 0x0F];
}
text[ti] = '\0';
return calogValueString(result, text, 36);
}

26
libs/calogCrypto.h Normal file
View file

@ -0,0 +1,26 @@
// calogCrypto.h -- calog cryptography library.
//
// Bridges binary-safe calog strings to OpenSSL's one-shot primitives, so any engine gets
// hashing, HMAC, randomness, and common encodings without an engine-specific library:
// hashSha256(data) -> lowercase hex digest (64 chars)
// hashSha1(data) -> lowercase hex digest (40 chars)
// hmacSha256(key, data) -> lowercase hex HMAC-SHA-256 (64 chars)
// randomBytes(count) -> binary string of count cryptographically-random bytes
// base64Encode(data) -> base64 text
// base64Decode(text) -> decoded binary string
// hexEncode(data) -> lowercase hex text
// hexDecode(hexText) -> decoded binary string
// uuid() -> random RFC 4122 version-4 UUID string
// All natives are binary-safe and INLINE (pure computation over OpenSSL one-shots, no shared
// state), so there is nothing to shut down.
#ifndef CALOG_CRYPTO_H
#define CALOG_CRYPTO_H
#include "calog.h"
// Register the crypto natives on a runtime. Stateless: safe to call on any number of
// runtimes, and there is no matching shutdown.
int32_t calogCryptoRegister(CalogT *calog);
#endif

352
libs/calogFs.c Normal file
View file

@ -0,0 +1,352 @@
// calogFs.c -- calog filesystem library (see calogFs.h). Thin, binary-safe wrappers over the
// POSIX file API (open/read/write, stat, opendir/readdir, mkdir, unlink), bridging paths and
// file bytes to calog's value/aggregate model. No shared state: every native is a pure function
// of its arguments over the OS filesystem, so the natives are INLINE and there is nothing to
// shut down. A system-call failure becomes a calogFail carrying strerror(errno).
#define _POSIX_C_SOURCE 200809L
#include "calogFs.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsFail(CalogValueT *result, int32_t err);
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value);
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result);
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogFsRegister(CalogT *calog) {
calogRegisterInline(calog, "fsRead", fsReadNative, NULL);
calogRegisterInline(calog, "fsWrite", fsWriteNative, NULL);
calogRegisterInline(calog, "fsAppend", fsAppendNative, NULL);
calogRegisterInline(calog, "fsExists", fsExistsNative, NULL);
calogRegisterInline(calog, "fsRemove", fsRemoveNative, NULL);
calogRegisterInline(calog, "fsMkdir", fsMkdirNative, NULL);
calogRegisterInline(calog, "fsList", fsListNative, NULL);
calogRegisterInline(calog, "fsStat", fsStatNative, NULL);
return calogOkE;
}
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsAppend expects (path, data)");
}
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, true, result);
}
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsExists expects (path)");
}
calogValueBool(result, access(args[0].as.s.bytes, F_OK) == 0);
return calogOkE;
}
// Map a failed syscall's errno to a calog status (ENOENT and the general case both read as
// "not found", ENOMEM as OOM) and carry the human-readable strerror text as the message.
static int32_t fsFail(CalogValueT *result, int32_t err) {
if (err == ENOMEM) {
return calogFail(result, calogErrOomE, strerror(err));
}
return calogFail(result, calogErrNotFoundE, strerror(err));
}
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct dirent *entry;
CalogValueT name;
CalogAggT *agg;
DIR *dir;
int32_t status;
int saved;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsList expects (path)");
}
dir = opendir(args[0].as.s.bytes);
if (dir == NULL) {
return fsFail(result, errno);
}
status = calogAggCreate(&agg, calogListE);
if (status != calogOkE) {
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
for (;;) {
errno = 0;
entry = readdir(dir);
if (entry == NULL) {
if (errno != 0) {
saved = errno;
calogAggFree(agg);
closedir(dir);
return fsFail(result, saved);
}
break;
}
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
status = calogValueString(&name, entry->d_name, (int64_t)strlen(entry->d_name));
if (status != calogOkE) {
calogAggFree(agg);
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
status = calogAggPush(agg, &name);
if (status != calogOkE) {
calogValueFree(&name);
calogAggFree(agg);
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
}
closedir(dir);
calogValueAgg(result, agg);
return calogOkE;
}
// Add keyName -> value to a map, taking ownership of value: on any failure both the freshly
// built key and the caller's value are freed, so the caller never double-frees.
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value) {
CalogValueT key;
int32_t status;
status = calogValueString(&key, keyName, (int64_t)strlen(keyName));
if (status != calogOkE) {
calogValueFree(value);
return status;
}
status = calogAggSet(agg, &key, value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(value);
return status;
}
return calogOkE;
}
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
const char *path;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsMkdir expects (path)");
}
path = args[0].as.s.bytes;
if (mkdir(path, 0777) != 0) {
// An already-existing directory is success; anything else (including a non-directory
// squatting on the name) is the failure the caller sees.
if (errno == EEXIST && stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
calogValueNil(result);
return calogOkE;
}
return fsFail(result, errno);
}
calogValueNil(result);
return calogOkE;
}
// Shared body of fsWrite (truncate) and fsAppend: create the file if needed and write exactly
// length bytes, honoring embedded NULs. On success result is nil.
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result) {
size_t offset;
size_t total;
int fd;
int flags;
int saved;
flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
fd = open(path, flags, 0666);
if (fd < 0) {
return fsFail(result, errno);
}
total = (length > 0) ? (size_t)length : (size_t)0;
offset = 0;
while (offset < total) {
ssize_t put;
put = write(fd, bytes + offset, total - offset);
if (put < 0) {
if (errno == EINTR) {
continue;
}
saved = errno;
close(fd);
return fsFail(result, saved);
}
offset += (size_t)put;
}
if (close(fd) != 0) {
return fsFail(result, errno);
}
calogValueNil(result);
return calogOkE;
}
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
const char *path;
char *data;
ssize_t got;
size_t offset;
size_t total;
int fd;
int saved;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsRead expects (path)");
}
path = args[0].as.s.bytes;
fd = open(path, O_RDONLY);
if (fd < 0) {
return fsFail(result, errno);
}
if (fstat(fd, &st) != 0) {
saved = errno;
close(fd);
return fsFail(result, saved);
}
total = (st.st_size > 0) ? (size_t)st.st_size : (size_t)0;
data = (char *)malloc(total + 1);
if (data == NULL) {
close(fd);
return calogFail(result, calogErrOomE, "fsRead: out of memory");
}
offset = 0;
while (offset < total) {
got = read(fd, data + offset, total - offset);
if (got < 0) {
if (errno == EINTR) {
continue;
}
saved = errno;
free(data);
close(fd);
return fsFail(result, saved);
}
if (got == 0) {
break;
}
offset += (size_t)got;
}
close(fd);
status = calogValueString(result, data, (int64_t)offset);
free(data);
return status;
}
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsRemove expects (path)");
}
// unlink refuses a directory (EISDIR on Linux, EPERM elsewhere); remove an empty
// directory with rmdir instead, so fsRemove deletes either a file or an empty directory.
if (unlink(args[0].as.s.bytes) == 0) {
return calogOkE;
}
if ((errno == EISDIR || errno == EPERM) && rmdir(args[0].as.s.bytes) == 0) {
return calogOkE;
}
return fsFail(result, errno);
}
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
CalogValueT value;
CalogAggT *agg;
const char *path;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsStat expects (path)");
}
path = args[0].as.s.bytes;
if (stat(path, &st) != 0) {
// A missing path is not an error: report nil so callers can probe with fsStat.
if (errno == ENOENT) {
calogValueNil(result);
return calogOkE;
}
return fsFail(result, errno);
}
status = calogAggCreate(&agg, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "fsStat: out of memory");
}
calogValueInt(&value, (int64_t)st.st_size);
status = fsMapSet(agg, "size", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueBool(&value, S_ISDIR(st.st_mode) != 0);
status = fsMapSet(agg, "isDir", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueBool(&value, S_ISREG(st.st_mode) != 0);
status = fsMapSet(agg, "isFile", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueInt(&value, (int64_t)st.st_mtime);
status = fsMapSet(agg, "mtime", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueAgg(result, agg);
return calogOkE;
}
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsWrite expects (path, data)");
}
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, false, result);
}

26
libs/calogFs.h Normal file
View file

@ -0,0 +1,26 @@
// calogFs.h -- calog filesystem library.
//
// Bridges the POSIX filesystem into calog's value model so any engine can read, write, and
// inspect files without an engine-specific I/O library:
// fsRead(path) -> string (the whole file, binary-safe)
// fsWrite(path, data) -> nil (create/truncate, honoring data's byte length)
// fsAppend(path, data) -> nil (create if absent, append at the end)
// fsExists(path) -> bool
// fsRemove(path) -> nil (unlink a file)
// fsMkdir(path) -> nil (single level, mode 0777 & umask; an existing dir is OK)
// fsList(path) -> list (entry name strings, excluding "." and "..")
// fsStat(path) -> map {size, isDir, isFile, mtime} or nil if the path is absent
// The natives are INLINE (each is a pure function of its arguments over the OS filesystem, no
// shared state), so there is nothing to shut down. A failed operation surfaces as a catchable
// script error carrying strerror(errno).
#ifndef CALOG_FS_H
#define CALOG_FS_H
#include "calog.h"
// Register the filesystem natives on a runtime. Stateless: safe to call on any number of
// runtimes, and there is no matching shutdown.
int32_t calogFsRegister(CalogT *calog);
#endif

984
libs/calogHttp.c Normal file
View file

@ -0,0 +1,984 @@
// calogHttp.c -- calog HTTP client library (see calogHttp.h). A self-contained HTTP/1.1
// client: parse the URL, connect (getaddrinfo + socket + connect), optionally wrap the socket
// in OpenSSL TLS for https, send the request with "Connection: close", read the entire
// response to EOF, and decode it (status line, lowercased headers into a map, and a body that
// honors Transfer-Encoding: chunked / Content-Length / read-to-close). No shared state: every
// native is a self-contained connection, so the natives are INLINE and there is nothing to
// shut down. TLS does NO certificate verification in v1 (documented in the header).
#define _GNU_SOURCE
#include "calogHttp.h"
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <openssl/ssl.h>
#define HTTP_BUF_INITIAL 256
#define HTTP_READ_CHUNK 16384
#define HTTP_MAX_RESPONSE (64 * 1024 * 1024)
#define HTTP_HOST_MAX 256
#define HTTP_PORT_MAX 16
#define HTTP_PATH_MAX 4096
// A growable output byte buffer for the request and for decoded bodies.
typedef struct HttpBufT {
char *bytes;
size_t length;
size_t cap;
} HttpBufT;
// A parsed absolute URL. host/port/path are NUL-terminated so they double as C strings.
typedef struct HttpUrlT {
bool https;
char host[HTTP_HOST_MAX];
char port[HTTP_PORT_MAX];
char path[HTTP_PATH_MAX];
} HttpUrlT;
// One connection: a plain socket, or a TLS session layered over it when ssl != NULL.
typedef struct HttpConnT {
int fd;
SSL *ssl;
SSL_CTX *ctx;
} HttpConnT;
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers);
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra);
static void httpBufFree(HttpBufT *buf);
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length);
static int32_t httpBufPutStr(HttpBufT *buf, const char *s);
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result);
static void httpConnClose(HttpConnT *conn);
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length);
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length);
static bool httpContainsChunked(const char *bytes, size_t length);
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length);
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out);
static bool httpDefaultPort(const HttpUrlT *url);
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpHexVal(char c);
static char httpLower(char c);
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner);
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value);
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length);
static int64_t httpParseInt(const char *bytes, size_t length);
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut);
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result);
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, CalogValueT *result);
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw);
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
int32_t calogHttpRegister(CalogT *calog) {
calogRegisterInline(calog, "httpGet", httpGetNative, NULL);
calogRegisterInline(calog, "httpRequest", httpRequestNative, NULL);
return calogOkE;
}
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers) {
int64_t index;
int32_t status;
// Only string:string pairs become request headers; anything else is skipped. The bytes are
// binary-safe (length-carried), so a header value may hold arbitrary octets.
for (index = 0; index < headers->pairCount; index++) {
const CalogValueT *key;
const CalogValueT *val;
key = &headers->pairs[index].key;
val = &headers->pairs[index].value;
if (key->type != calogStringE || val->type != calogStringE) {
continue;
}
status = httpBufPutBytes(buf, key->as.s.bytes, (size_t)key->as.s.length);
if (status == calogOkE) {
status = httpBufPutStr(buf, ": ");
}
if (status == calogOkE) {
status = httpBufPutBytes(buf, val->as.s.bytes, (size_t)val->as.s.length);
}
if (status == calogOkE) {
status = httpBufPutStr(buf, "\r\n");
}
if (status != calogOkE) {
return status;
}
}
return calogOkE;
}
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra) {
size_t need;
size_t cap;
char *grown;
need = buf->length + extra;
if (need <= buf->cap) {
return calogOkE;
}
cap = (buf->cap == 0) ? HTTP_BUF_INITIAL : buf->cap;
while (cap < need) {
cap *= CALOG_GROWTH_FACTOR;
}
grown = (char *)realloc(buf->bytes, cap);
if (grown == NULL) {
return calogErrOomE;
}
buf->bytes = grown;
buf->cap = cap;
return calogOkE;
}
static void httpBufFree(HttpBufT *buf) {
free(buf->bytes);
buf->bytes = NULL;
buf->length = 0;
buf->cap = 0;
}
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length) {
int32_t status;
if (length == 0) {
return calogOkE;
}
status = httpBufEnsure(buf, length);
if (status != calogOkE) {
return status;
}
memcpy(buf->bytes + buf->length, bytes, length);
buf->length += length;
return calogOkE;
}
static int32_t httpBufPutStr(HttpBufT *buf, const char *s) {
return httpBufPutBytes(buf, s, strlen(s));
}
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result) {
const char *sep;
const char *headerBlock;
const char *bodyRaw;
const char *statusEnd;
const char *cursor;
const char *headerEnd;
const char *finalBody;
HttpBufT dechunked;
CalogAggT *map;
CalogAggT *headersMap;
size_t headerLen;
size_t bodyRawLen;
size_t statusLen;
size_t finalLen;
int64_t contentLength;
int32_t code;
int32_t status;
bool haveContentLength;
bool chunked;
calogValueNil(result);
dechunked.bytes = NULL;
dechunked.length = 0;
dechunked.cap = 0;
contentLength = 0;
haveContentLength = false;
chunked = false;
// Split the head from the body at the first blank line.
sep = (const char *)memmem(raw, rawLen, "\r\n\r\n", 4);
if (sep == NULL) {
return calogFail(result, calogErrArgE, "http: malformed response (no header terminator)");
}
headerBlock = raw;
headerLen = (size_t)(sep - raw);
bodyRaw = sep + 4;
bodyRawLen = rawLen - headerLen - 4;
// The status line is the first CRLF-terminated line of the head.
statusEnd = (const char *)memmem(headerBlock, headerLen, "\r\n", 2);
if (statusEnd == NULL) {
statusLen = headerLen;
cursor = headerBlock + headerLen;
} else {
statusLen = (size_t)(statusEnd - headerBlock);
cursor = statusEnd + 2;
}
headerEnd = headerBlock + headerLen;
status = httpParseStatus(headerBlock, statusLen, &code);
if (status != calogOkE) {
return calogFail(result, calogErrArgE, "http: malformed status line");
}
// Header fields -> map keyed by lowercased name; capture the body-framing headers.
status = calogAggCreate(&headersMap, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "http: out of memory");
}
while (cursor < headerEnd) {
const char *lineEnd;
const char *colon;
size_t lineLen;
lineEnd = (const char *)memmem(cursor, (size_t)(headerEnd - cursor), "\r\n", 2);
if (lineEnd == NULL) {
lineEnd = headerEnd;
}
lineLen = (size_t)(lineEnd - cursor);
colon = (lineLen > 0) ? (const char *)memchr(cursor, ':', lineLen) : NULL;
if (colon != NULL) {
const char *nameStart;
const char *valStart;
char *lower;
size_t nameLen;
size_t valLen;
size_t i;
nameStart = cursor;
nameLen = (size_t)(colon - cursor);
valStart = colon + 1;
valLen = (size_t)(lineEnd - valStart);
while (valLen > 0 && (*valStart == ' ' || *valStart == '\t')) {
valStart++;
valLen--;
}
while (valLen > 0 && (valStart[valLen - 1] == ' ' || valStart[valLen - 1] == '\t')) {
valLen--;
}
lower = (char *)malloc(nameLen + 1);
if (lower == NULL) {
calogAggFree(headersMap);
return calogFail(result, calogErrOomE, "http: out of memory");
}
for (i = 0; i < nameLen; i++) {
lower[i] = httpLower(nameStart[i]);
}
lower[nameLen] = '\0';
status = httpMapSetStr(headersMap, lower, valStart, (int64_t)valLen);
if (status != calogOkE) {
free(lower);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
if (strcmp(lower, "content-length") == 0) {
contentLength = httpParseInt(valStart, valLen);
if (contentLength >= 0) {
haveContentLength = true;
}
} else if (strcmp(lower, "transfer-encoding") == 0) {
if (httpContainsChunked(valStart, valLen)) {
chunked = true;
}
}
free(lower);
}
if (lineEnd >= headerEnd) {
break;
}
cursor = lineEnd + 2;
}
// Decode the body. Chunked takes precedence over Content-Length; absent both, read-to-close
// means the raw remainder is the body.
if (chunked) {
status = httpDechunk(bodyRaw, bodyRawLen, &dechunked);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(headersMap);
return calogFail(result, status, "http: malformed chunked body");
}
finalBody = dechunked.bytes;
finalLen = dechunked.length;
} else if (haveContentLength) {
finalBody = bodyRaw;
finalLen = ((size_t)contentLength < bodyRawLen) ? (size_t)contentLength : bodyRawLen;
} else {
finalBody = bodyRaw;
finalLen = bodyRawLen;
}
// Assemble the outer { status, body, headers } map.
status = calogAggCreate(&map, calogMapE);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetInt(map, "status", (int64_t)code);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetStr(map, "body", (finalBody != NULL) ? finalBody : "", (int64_t)finalLen);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
status = httpMapSetAgg(map, "headers", headersMap);
if (status != calogOkE) {
httpBufFree(&dechunked);
calogAggFree(map);
calogAggFree(headersMap);
return calogFail(result, status, "http: out of memory");
}
// headersMap is now owned by map; only our transient decode buffer remains to free.
httpBufFree(&dechunked);
calogValueAgg(result, map);
return calogOkE;
}
static void httpConnClose(HttpConnT *conn) {
if (conn->ssl != NULL) {
SSL_shutdown(conn->ssl);
SSL_free(conn->ssl);
conn->ssl = NULL;
}
if (conn->ctx != NULL) {
SSL_CTX_free(conn->ctx);
conn->ctx = NULL;
}
// SSL_set_fd installs a BIO_NOCLOSE socket BIO, so SSL_free never closes the fd -- we own it.
if (conn->fd >= 0) {
close(conn->fd);
conn->fd = -1;
}
}
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
struct addrinfo hints;
struct addrinfo *res;
struct addrinfo *rp;
int fd;
int rc;
conn->fd = -1;
conn->ssl = NULL;
conn->ctx = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
rc = getaddrinfo(url->host, url->port, &hints, &res);
if (rc != 0) {
return calogFail(result, calogErrArgE, gai_strerror(rc));
}
fd = -1;
for (rp = res; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) {
continue;
}
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0) {
return calogFail(result, calogErrArgE, "http: could not connect to host");
}
conn->fd = fd;
if (url->https) {
int32_t status;
status = httpTlsHandshake(conn, url, result);
if (status != calogOkE) {
httpConnClose(conn);
return status;
}
}
return calogOkE;
}
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length) {
if (conn->ssl != NULL) {
int chunk;
int r;
chunk = (length > (size_t)INT_MAX) ? INT_MAX : (int)length;
r = SSL_read(conn->ssl, buf, chunk);
// A non-positive return (clean shutdown or error) ends the read-to-EOF loop.
if (r <= 0) {
return 0;
}
return (ssize_t)r;
}
for (;;) {
ssize_t r;
r = recv(conn->fd, buf, length, 0);
if (r < 0 && errno == EINTR) {
continue;
}
return r;
}
}
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length) {
size_t sent;
sent = 0;
while (sent < length) {
if (conn->ssl != NULL) {
int chunk;
int w;
chunk = (length - sent > (size_t)INT_MAX) ? INT_MAX : (int)(length - sent);
w = SSL_write(conn->ssl, bytes + sent, chunk);
if (w <= 0) {
return calogErrUnsupportedE;
}
sent += (size_t)w;
} else {
ssize_t w;
w = send(conn->fd, bytes + sent, length - sent, MSG_NOSIGNAL);
if (w < 0) {
if (errno == EINTR) {
continue;
}
return calogErrArgE;
}
sent += (size_t)w;
}
}
return calogOkE;
}
static bool httpContainsChunked(const char *bytes, size_t length) {
static const char needle[] = "chunked";
size_t nlen;
size_t i;
nlen = sizeof(needle) - 1;
if (length < nlen) {
return false;
}
for (i = 0; i + nlen <= length; i++) {
size_t j;
bool match;
match = true;
for (j = 0; j < nlen; j++) {
if (httpLower(bytes[i + j]) != needle[j]) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
}
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length) {
if (length + 1 > cap) {
return false;
}
memcpy(dst, src, length);
dst[length] = '\0';
return true;
}
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out) {
size_t pos;
pos = 0;
while (pos < length) {
size_t size;
bool any;
int32_t status;
size = 0;
any = false;
while (pos < length) {
int32_t digit;
digit = httpHexVal(bytes[pos]);
if (digit < 0) {
break;
}
// Reject a chunk size that would overflow size_t (a hostile server can send a huge
// hex length to wrap the bounds check below).
if (size > (SIZE_MAX - (size_t)digit) / 16) {
return calogErrArgE;
}
size = size * 16 + (size_t)digit;
any = true;
pos++;
}
if (!any) {
return calogErrArgE;
}
// Skip any chunk extension (";name=value") through the CRLF that ends the size line.
while (pos < length && bytes[pos] != '\n') {
pos++;
}
if (pos >= length) {
return calogErrArgE;
}
pos++;
if (size == 0) {
break;
}
if (size > length - pos) { // pos <= length here, so no overflow
return calogErrArgE;
}
status = httpBufPutBytes(out, bytes + pos, size);
if (status != calogOkE) {
return status;
}
pos += size;
if (pos < length && bytes[pos] == '\r') {
pos++;
}
if (pos < length && bytes[pos] == '\n') {
pos++;
}
}
return calogOkE;
}
static bool httpDefaultPort(const HttpUrlT *url) {
if (url->https) {
return strcmp(url->port, "443") == 0;
}
return strcmp(url->port, "80") == 0;
}
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "httpGet expects (url)");
}
return httpPerform("GET", args[0].as.s.bytes, args[0].as.s.length, NULL, NULL, 0, result);
}
static int32_t httpHexVal(char c) {
if (c >= '0' && c <= '9') {
return (int32_t)(c - '0');
}
if (c >= 'a' && c <= 'f') {
return (int32_t)(c - 'a' + 10);
}
if (c >= 'A' && c <= 'F') {
return (int32_t)(c - 'A' + 10);
}
return -1;
}
static char httpLower(char c) {
if (c >= 'A' && c <= 'Z') {
return (char)(c - 'A' + 'a');
}
return c;
}
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner) {
CalogValueT keyValue;
CalogValueT aggValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
calogValueAgg(&aggValue, inner);
status = calogAggSet(map, &keyValue, &aggValue);
if (status != calogOkE) {
// calogAggSet did not take ownership; free the key but leave `inner` (which aggValue
// only wraps) for the caller to free.
calogValueFree(&keyValue);
}
return status;
}
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value) {
CalogValueT keyValue;
CalogValueT intValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
calogValueInt(&intValue, value);
status = calogAggSet(map, &keyValue, &intValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
}
return status;
}
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length) {
CalogValueT keyValue;
CalogValueT stringValue;
int32_t status;
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
if (status != calogOkE) {
return status;
}
status = calogValueString(&stringValue, bytes, length);
if (status != calogOkE) {
calogValueFree(&keyValue);
return status;
}
status = calogAggSet(map, &keyValue, &stringValue);
if (status != calogOkE) {
calogValueFree(&keyValue);
calogValueFree(&stringValue);
}
return status;
}
static int64_t httpParseInt(const char *bytes, size_t length) {
int64_t value;
size_t i;
bool any;
value = 0;
any = false;
for (i = 0; i < length; i++) {
if (bytes[i] < '0' || bytes[i] > '9') {
break;
}
value = value * 10 + (int64_t)(bytes[i] - '0');
any = true;
if (value > (int64_t)HTTP_MAX_RESPONSE) {
break;
}
}
if (!any) {
return -1;
}
return value;
}
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut) {
int32_t code;
size_t i;
bool any;
i = 0;
// Skip the "HTTP/x.y" token and the space(s) before the status code.
while (i < length && line[i] != ' ') {
i++;
}
while (i < length && line[i] == ' ') {
i++;
}
code = 0;
any = false;
while (i < length && line[i] >= '0' && line[i] <= '9') {
code = code * 10 + (int32_t)(line[i] - '0');
any = true;
i++;
}
if (!any) {
return calogErrArgE;
}
*codeOut = code;
return calogOkE;
}
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result) {
size_t len;
size_t pos;
size_t hostStart;
size_t hostEnd;
len = (size_t)urlLen;
if (len >= 7 && memcmp(url, "http://", 7) == 0) {
out->https = false;
pos = 7;
} else if (len >= 8 && memcmp(url, "https://", 8) == 0) {
out->https = true;
pos = 8;
} else {
return calogFail(result, calogErrArgE, "http: URL must start with http:// or https://");
}
hostStart = pos;
while (pos < len && url[pos] != ':' && url[pos] != '/') {
pos++;
}
hostEnd = pos;
if (hostEnd == hostStart) {
return calogFail(result, calogErrArgE, "http: URL has an empty host");
}
if (!httpCopyField(out->host, sizeof(out->host), url + hostStart, hostEnd - hostStart)) {
return calogFail(result, calogErrArgE, "http: URL host too long");
}
if (pos < len && url[pos] == ':') {
size_t portStart;
pos++;
portStart = pos;
while (pos < len && url[pos] != '/') {
pos++;
}
if (!httpCopyField(out->port, sizeof(out->port), url + portStart, pos - portStart)) {
return calogFail(result, calogErrArgE, "http: URL port too long");
}
if (out->port[0] == '\0') {
return calogFail(result, calogErrArgE, "http: URL has an empty port");
}
} else {
strcpy(out->port, out->https ? "443" : "80");
}
if (pos < len) {
if (!httpCopyField(out->path, sizeof(out->path), url + pos, len - pos)) {
return calogFail(result, calogErrArgE, "http: URL path too long");
}
} else {
strcpy(out->path, "/");
}
return calogOkE;
}
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, CalogValueT *result) {
HttpUrlT url;
HttpConnT conn;
HttpBufT request;
HttpBufT raw;
int32_t status;
calogValueNil(result);
request.bytes = NULL;
request.length = 0;
request.cap = 0;
raw.bytes = NULL;
raw.length = 0;
raw.cap = 0;
status = httpParseUrl(urlBytes, urlLen, &url, result);
if (status != calogOkE) {
return status;
}
// Request line + Host + Connection: close.
status = httpBufPutStr(&request, method);
if (status == calogOkE) {
status = httpBufPutStr(&request, " ");
}
if (status == calogOkE) {
status = httpBufPutStr(&request, url.path);
}
if (status == calogOkE) {
status = httpBufPutStr(&request, " HTTP/1.1\r\nHost: ");
}
if (status == calogOkE) {
status = httpBufPutStr(&request, url.host);
}
if (status == calogOkE && !httpDefaultPort(&url)) {
status = httpBufPutStr(&request, ":");
if (status == calogOkE) {
status = httpBufPutStr(&request, url.port);
}
}
if (status == calogOkE) {
status = httpBufPutStr(&request, "\r\nConnection: close\r\n");
}
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
// Caller-supplied headers.
if (headers != NULL) {
status = httpAppendHeaders(&request, headers);
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
}
// Content-Length + the blank line + body.
if (bodyLen > 0) {
char lengthLine[64];
int n;
n = snprintf(lengthLine, sizeof(lengthLine), "Content-Length: %lld\r\n", (long long)bodyLen);
status = httpBufPutBytes(&request, lengthLine, (size_t)n);
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
}
status = httpBufPutStr(&request, "\r\n");
if (status == calogOkE && bodyLen > 0) {
status = httpBufPutBytes(&request, body, (size_t)bodyLen);
}
if (status != calogOkE) {
httpBufFree(&request);
return calogFail(result, status, "http: out of memory");
}
status = httpConnOpen(&conn, &url, result);
if (status != calogOkE) {
httpBufFree(&request);
return status;
}
status = httpConnWriteAll(&conn, request.bytes, request.length);
httpBufFree(&request);
if (status != calogOkE) {
httpConnClose(&conn);
return calogFail(result, status, "http: failed to send request");
}
status = httpReadResponse(&conn, &raw);
httpConnClose(&conn);
if (status != calogOkE) {
httpBufFree(&raw);
return calogFail(result, status, "http: failed to read response");
}
status = httpBuildResult((raw.bytes != NULL) ? raw.bytes : "", raw.length, result);
httpBufFree(&raw);
return status;
}
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw) {
char chunk[HTTP_READ_CHUNK];
// "Connection: close" means the server closes at end of message, so reading to EOF yields
// the entire response (head + body).
for (;;) {
ssize_t n;
int32_t status;
n = httpConnRead(conn, chunk, sizeof(chunk));
if (n <= 0) {
break;
}
if (raw->length + (size_t)n > (size_t)HTTP_MAX_RESPONSE) {
return calogErrRangeE;
}
status = httpBufPutBytes(raw, chunk, (size_t)n);
if (status != calogOkE) {
return status;
}
}
return calogOkE;
}
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *opts;
CalogValueT *urlValue;
CalogValueT *methodValue;
CalogValueT *headersValue;
CalogValueT *bodyValue;
CalogValueT keyValue;
const CalogAggT *headers;
const char *method;
const char *body;
int64_t bodyLen;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogAggE) {
return calogFail(result, calogErrArgE, "httpRequest expects (options)");
}
opts = args[0].as.agg;
// url (required).
status = calogValueString(&keyValue, "url", 3);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
urlValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (urlValue == NULL || urlValue->type != calogStringE) {
return calogFail(result, calogErrArgE, "httpRequest: options.url must be a string");
}
// method (optional; default GET).
method = "GET";
status = calogValueString(&keyValue, "method", 6);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
methodValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (methodValue != NULL && methodValue->type == calogStringE) {
method = methodValue->as.s.bytes;
}
// headers (optional).
headers = NULL;
status = calogValueString(&keyValue, "headers", 7);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
headersValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (headersValue != NULL && headersValue->type == calogAggE) {
headers = headersValue->as.agg;
}
// body (optional).
body = NULL;
bodyLen = 0;
status = calogValueString(&keyValue, "body", 4);
if (status != calogOkE) {
return calogFail(result, status, "httpRequest: out of memory");
}
bodyValue = calogAggGet(opts, &keyValue);
calogValueFree(&keyValue);
if (bodyValue != NULL && bodyValue->type == calogStringE) {
body = bodyValue->as.s.bytes;
bodyLen = bodyValue->as.s.length;
}
return httpPerform(method, urlValue->as.s.bytes, urlValue->as.s.length, headers, body, bodyLen, result);
}
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
conn->ctx = SSL_CTX_new(TLS_client_method());
if (conn->ctx == NULL) {
return calogFail(result, calogErrUnsupportedE, "http: TLS context creation failed");
}
conn->ssl = SSL_new(conn->ctx);
if (conn->ssl == NULL) {
return calogFail(result, calogErrUnsupportedE, "http: TLS session creation failed");
}
if (SSL_set_fd(conn->ssl, conn->fd) != 1) {
return calogFail(result, calogErrUnsupportedE, "http: TLS SSL_set_fd failed");
}
// SNI so name-based virtual hosts serve the right certificate. NOTE: v1 does NO certificate
// verification -- https here is transport encryption only, not server authentication.
(void)SSL_set_tlsext_host_name(conn->ssl, url->host);
if (SSL_connect(conn->ssl) != 1) {
return calogFail(result, calogErrUnsupportedE, "http: TLS handshake failed");
}
return calogOkE;
}

26
libs/calogHttp.h Normal file
View file

@ -0,0 +1,26 @@
// calogHttp.h -- calog HTTP client library.
//
// Bridges calog scripts to a minimal HTTP/1.1 client so any engine can fetch and post over
// http:// and https:// without an engine-specific library:
// httpGet(url) -> { status, body, headers }
// httpRequest(opts) -> { status, body, headers }
// where opts is a map { method (default "GET"), url, headers (map of name->value), body }.
// The result map carries the integer status code, the binary-safe response body, and a
// headers map keyed by the lowercased header name. Each call opens its own connection
// ("Connection: close"), reads the whole response to EOF, and decodes the body honoring
// Transfer-Encoding: chunked / Content-Length / read-to-close. Redirects are NOT followed
// (a 3xx is returned as-is). For https:// the TLS handshake uses SNI but performs NO
// certificate verification (v1 -- treat https as transport encoding only, not authenticated).
// The natives are INLINE (each call is a self-contained connection with no shared state), so
// there is nothing to shut down.
#ifndef CALOG_HTTP_H
#define CALOG_HTTP_H
#include "calog.h"
// Register the HTTP natives (httpGet, httpRequest) on a runtime. Stateless: safe to call on
// any number of runtimes, and there is no matching shutdown.
int32_t calogHttpRegister(CalogT *calog);
#endif

762
libs/calogJson.c Normal file
View file

@ -0,0 +1,762 @@
// calogJson.c -- calog JSON library (see calogJson.h). A self-contained recursive-descent
// parser and a serializer, both bridging JSON text and calog's value/aggregate model. No
// shared state: the two natives are pure functions of their arguments.
#define _POSIX_C_SOURCE 200809L
#include "calogJson.h"
#include <errno.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define JSON_BUF_INITIAL 64
// A growable output byte buffer for serialization and for decoded string bodies.
typedef struct JsonBufT {
char *bytes;
size_t length;
size_t cap;
} JsonBufT;
// A read cursor over the source text (binary-safe: bounded by end, not a NUL).
typedef struct JsonParseT {
const char *cur;
const char *end;
} JsonParseT;
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra);
static void jsonBufFree(JsonBufT *buf);
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length);
static int32_t jsonBufPutChar(JsonBufT *buf, char c);
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth);
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth);
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key);
static int32_t jsonEncodeReal(JsonBufT *buf, double r);
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length);
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out);
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out);
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf);
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth);
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp);
static void jsonSkipWs(JsonParseT *p);
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogJsonRegister(CalogT *calog) {
calogRegisterInline(calog, "jsonParse", jsonParseNative, NULL);
calogRegisterInline(calog, "jsonStringify", jsonStringifyNative, NULL);
return calogOkE;
}
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra) {
size_t need;
size_t cap;
char *grown;
need = buf->length + extra;
if (need <= buf->cap) {
return calogOkE;
}
cap = (buf->cap == 0) ? JSON_BUF_INITIAL : buf->cap;
while (cap < need) {
cap *= CALOG_GROWTH_FACTOR;
}
grown = (char *)realloc(buf->bytes, cap);
if (grown == NULL) {
return calogErrOomE;
}
buf->bytes = grown;
buf->cap = cap;
return calogOkE;
}
static void jsonBufFree(JsonBufT *buf) {
free(buf->bytes);
buf->bytes = NULL;
buf->length = 0;
buf->cap = 0;
}
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length) {
int32_t status;
if (length == 0) {
return calogOkE;
}
status = jsonBufEnsure(buf, length);
if (status != calogOkE) {
return status;
}
memcpy(buf->bytes + buf->length, bytes, length);
buf->length += length;
return calogOkE;
}
static int32_t jsonBufPutChar(JsonBufT *buf, char c) {
return jsonBufPutBytes(buf, &c, 1);
}
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth) {
char tmp[32];
int n;
if (depth >= CALOG_MAX_DEPTH) {
return calogErrDepthE;
}
switch (value->type) {
case calogNilE:
return jsonBufPutBytes(buf, "null", 4);
case calogBoolE:
return value->as.b ? jsonBufPutBytes(buf, "true", 4) : jsonBufPutBytes(buf, "false", 5);
case calogIntE:
n = snprintf(tmp, sizeof(tmp), "%lld", (long long)value->as.i);
return jsonBufPutBytes(buf, tmp, (size_t)n);
case calogRealE:
return jsonEncodeReal(buf, value->as.r);
case calogStringE:
return jsonEncodeString(buf, value->as.s.bytes, value->as.s.length);
case calogAggE:
return jsonEncodeAgg(buf, value->as.agg, depth);
case calogFnE:
return calogErrTypeE;
default:
return calogErrTypeE;
}
}
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth) {
int64_t index;
int32_t status;
// A map (or a hybrid that carries keyed pairs) serializes as a JSON object; a pure
// sequence serializes as a JSON array. In the hybrid case the array elements are emitted
// under their integer indices so nothing is silently dropped.
if (agg->pairCount > 0 || agg->kind == calogMapE) {
bool first;
first = true;
status = jsonBufPutChar(buf, '{');
if (status != calogOkE) {
return status;
}
for (index = 0; index < agg->arrayCount; index++) {
char tmp[32];
int n;
if (!first) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
first = false;
n = snprintf(tmp, sizeof(tmp), "\"%lld\":", (long long)index);
status = jsonBufPutBytes(buf, tmp, (size_t)n);
if (status != calogOkE) {
return status;
}
status = jsonEncode(buf, &agg->array[index], depth + 1);
if (status != calogOkE) {
return status;
}
}
for (index = 0; index < agg->pairCount; index++) {
if (!first) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
first = false;
status = jsonEncodeKey(buf, &agg->pairs[index].key);
if (status != calogOkE) {
return status;
}
status = jsonBufPutChar(buf, ':');
if (status != calogOkE) {
return status;
}
status = jsonEncode(buf, &agg->pairs[index].value, depth + 1);
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, '}');
}
status = jsonBufPutChar(buf, '[');
if (status != calogOkE) {
return status;
}
for (index = 0; index < agg->arrayCount; index++) {
if (index > 0) {
status = jsonBufPutChar(buf, ',');
if (status != calogOkE) {
return status;
}
}
status = jsonEncode(buf, &agg->array[index], depth + 1);
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, ']');
}
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key) {
char tmp[32];
int n;
// JSON object keys must be strings; coerce a scalar key to its string form.
switch (key->type) {
case calogStringE:
return jsonEncodeString(buf, key->as.s.bytes, key->as.s.length);
case calogIntE:
n = snprintf(tmp, sizeof(tmp), "\"%lld\"", (long long)key->as.i);
return jsonBufPutBytes(buf, tmp, (size_t)n);
case calogBoolE:
return key->as.b ? jsonBufPutBytes(buf, "\"true\"", 6) : jsonBufPutBytes(buf, "\"false\"", 7);
case calogRealE:
n = snprintf(tmp, sizeof(tmp), "\"%.17g\"", key->as.r);
return jsonBufPutBytes(buf, tmp, (size_t)n);
default:
return calogErrTypeE;
}
}
static int32_t jsonEncodeReal(JsonBufT *buf, double r) {
char tmp[40];
int n;
if (!isfinite(r)) {
return jsonBufPutBytes(buf, "null", 4);
}
n = snprintf(tmp, sizeof(tmp), "%.15g", r);
if (strtod(tmp, NULL) != r) {
n = snprintf(tmp, sizeof(tmp), "%.17g", r);
}
return jsonBufPutBytes(buf, tmp, (size_t)n);
}
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length) {
int64_t index;
int32_t status;
status = jsonBufPutChar(buf, '"');
if (status != calogOkE) {
return status;
}
for (index = 0; index < length; index++) {
unsigned char c;
c = (unsigned char)bytes[index];
switch (c) {
case '"':
status = jsonBufPutBytes(buf, "\\\"", 2);
break;
case '\\':
status = jsonBufPutBytes(buf, "\\\\", 2);
break;
case '\b':
status = jsonBufPutBytes(buf, "\\b", 2);
break;
case '\f':
status = jsonBufPutBytes(buf, "\\f", 2);
break;
case '\n':
status = jsonBufPutBytes(buf, "\\n", 2);
break;
case '\r':
status = jsonBufPutBytes(buf, "\\r", 2);
break;
case '\t':
status = jsonBufPutBytes(buf, "\\t", 2);
break;
default:
if (c < 0x20) {
char esc[8];
int n;
n = snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)c);
status = jsonBufPutBytes(buf, esc, (size_t)n);
} else {
status = jsonBufPutChar(buf, (char)c);
}
break;
}
if (status != calogOkE) {
return status;
}
}
return jsonBufPutChar(buf, '"');
}
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out) {
uint32_t value;
int i;
value = 0;
for (i = 0; i < 4; i++) {
int digit;
if (p->cur >= p->end) {
return calogErrArgE;
}
if (*p->cur >= '0' && *p->cur <= '9') {
digit = *p->cur - '0';
} else if (*p->cur >= 'a' && *p->cur <= 'f') {
digit = *p->cur - 'a' + 10;
} else if (*p->cur >= 'A' && *p->cur <= 'F') {
digit = *p->cur - 'A' + 10;
} else {
return calogErrArgE;
}
value = (value << 4) | (uint32_t)digit;
p->cur++;
}
*out = value;
return calogOkE;
}
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth) {
CalogAggT *agg;
int32_t status;
calogValueNil(out);
status = calogAggCreate(&agg, calogListE);
if (status != calogOkE) {
return status;
}
p->cur++; // consume '['
jsonSkipWs(p);
if (p->cur < p->end && *p->cur == ']') {
p->cur++;
calogValueAgg(out, agg);
return calogOkE;
}
for (;;) {
CalogValueT element;
status = jsonParseValue(p, &element, depth + 1);
if (status != calogOkE) {
calogAggFree(agg);
return status;
}
status = calogAggPush(agg, &element);
if (status != calogOkE) {
calogValueFree(&element);
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
calogAggFree(agg);
return calogErrArgE;
}
if (*p->cur == ',') {
p->cur++;
continue;
}
if (*p->cur == ']') {
p->cur++;
break;
}
calogAggFree(agg);
return calogErrArgE;
}
calogValueAgg(out, agg);
return calogOkE;
}
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out) {
size_t remaining;
calogValueNil(out);
remaining = (size_t)(p->end - p->cur);
if (remaining >= 4 && memcmp(p->cur, "true", 4) == 0) {
calogValueBool(out, true);
p->cur += 4;
return calogOkE;
}
if (remaining >= 5 && memcmp(p->cur, "false", 5) == 0) {
calogValueBool(out, false);
p->cur += 5;
return calogOkE;
}
if (remaining >= 4 && memcmp(p->cur, "null", 4) == 0) {
calogValueNil(out);
p->cur += 4;
return calogOkE;
}
return calogErrArgE;
}
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
JsonParseT parse;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "jsonParse expects (text)");
}
parse.cur = args[0].as.s.bytes;
parse.end = args[0].as.s.bytes + args[0].as.s.length;
status = jsonParseValue(&parse, result, 0);
if (status != calogOkE) {
calogValueFree(result);
return calogFail(result, status, "jsonParse: invalid JSON");
}
jsonSkipWs(&parse);
if (parse.cur != parse.end) {
calogValueFree(result);
return calogFail(result, calogErrArgE, "jsonParse: trailing characters after JSON value");
}
return calogOkE;
}
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out) {
const char *start;
char *stop;
bool isReal;
calogValueNil(out);
start = p->cur;
isReal = false;
if (p->cur < p->end && *p->cur == '-') {
p->cur++;
}
while (p->cur < p->end) {
char c;
c = *p->cur;
if (c >= '0' && c <= '9') {
p->cur++;
} else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') {
isReal = true;
p->cur++;
} else {
break;
}
}
if (p->cur == start) {
return calogErrArgE;
}
if (isReal) {
double r;
r = strtod(start, &stop);
if (stop != p->cur) {
return calogErrArgE;
}
calogValueReal(out, r);
return calogOkE;
}
{
long long i;
errno = 0;
i = strtoll(start, &stop, 10);
if (stop != p->cur) {
return calogErrArgE;
}
if (errno == ERANGE) {
// An integer literal that overflows int64 becomes a real, rather than silently
// saturating to LLONG_MAX/MIN.
double r;
r = strtod(start, &stop);
if (stop != p->cur) {
return calogErrArgE;
}
calogValueReal(out, r);
return calogOkE;
}
calogValueInt(out, (int64_t)i);
return calogOkE;
}
}
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth) {
CalogAggT *agg;
int32_t status;
calogValueNil(out);
status = calogAggCreate(&agg, calogMapE);
if (status != calogOkE) {
return status;
}
p->cur++; // consume '{'
jsonSkipWs(p);
if (p->cur < p->end && *p->cur == '}') {
p->cur++;
calogValueAgg(out, agg);
return calogOkE;
}
for (;;) {
CalogValueT key;
CalogValueT value;
jsonSkipWs(p);
if (p->cur >= p->end || *p->cur != '"') {
calogAggFree(agg);
return calogErrArgE;
}
status = jsonParseString(p, &key);
if (status != calogOkE) {
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end || *p->cur != ':') {
calogValueFree(&key);
calogAggFree(agg);
return calogErrArgE;
}
p->cur++; // consume ':'
status = jsonParseValue(p, &value, depth + 1);
if (status != calogOkE) {
calogValueFree(&key);
calogAggFree(agg);
return status;
}
status = calogAggSet(agg, &key, &value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(&value);
calogAggFree(agg);
return status;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
calogAggFree(agg);
return calogErrArgE;
}
if (*p->cur == ',') {
p->cur++;
continue;
}
if (*p->cur == '}') {
p->cur++;
break;
}
calogAggFree(agg);
return calogErrArgE;
}
calogValueAgg(out, agg);
return calogOkE;
}
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out) {
JsonBufT buf;
int32_t status;
calogValueNil(out);
buf.bytes = NULL;
buf.length = 0;
buf.cap = 0;
p->cur++; // consume opening '"'
while (p->cur < p->end) {
unsigned char c;
c = (unsigned char)*p->cur;
if (c == '"') {
p->cur++;
status = calogValueString(out, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
jsonBufFree(&buf);
return status;
}
if (c == '\\') {
char e;
p->cur++;
if (p->cur >= p->end) {
jsonBufFree(&buf);
return calogErrArgE;
}
e = *p->cur;
switch (e) {
case '"':
status = jsonBufPutChar(&buf, '"');
break;
case '\\':
status = jsonBufPutChar(&buf, '\\');
break;
case '/':
status = jsonBufPutChar(&buf, '/');
break;
case 'b':
status = jsonBufPutChar(&buf, '\b');
break;
case 'f':
status = jsonBufPutChar(&buf, '\f');
break;
case 'n':
status = jsonBufPutChar(&buf, '\n');
break;
case 'r':
status = jsonBufPutChar(&buf, '\r');
break;
case 't':
status = jsonBufPutChar(&buf, '\t');
break;
case 'u':
status = jsonParseUnicode(p, &buf);
break;
default:
jsonBufFree(&buf);
return calogErrArgE;
}
if (status != calogOkE) {
jsonBufFree(&buf);
return status;
}
if (e != 'u') {
p->cur++;
}
continue;
}
if (c < 0x20) {
jsonBufFree(&buf);
return calogErrArgE;
}
status = jsonBufPutChar(&buf, (char)c);
if (status != calogOkE) {
jsonBufFree(&buf);
return status;
}
p->cur++;
}
jsonBufFree(&buf);
return calogErrArgE;
}
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf) {
uint32_t cp;
int32_t status;
p->cur++; // consume 'u'
status = jsonHexQuad(p, &cp);
if (status != calogOkE) {
return status;
}
if (cp >= 0xD800 && cp <= 0xDBFF) {
uint32_t lo;
if (p->cur + 1 >= p->end || p->cur[0] != '\\' || p->cur[1] != 'u') {
return calogErrArgE;
}
p->cur += 2; // consume "\u"
status = jsonHexQuad(p, &lo);
if (status != calogOkE) {
return status;
}
if (lo < 0xDC00 || lo > 0xDFFF) {
return calogErrArgE;
}
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
return calogErrArgE; // an unpaired low surrogate is not a valid code point
}
return jsonPutUtf8(buf, cp);
}
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth) {
calogValueNil(out);
if (depth >= CALOG_MAX_DEPTH) {
return calogErrDepthE;
}
jsonSkipWs(p);
if (p->cur >= p->end) {
return calogErrArgE;
}
switch (*p->cur) {
case '{':
return jsonParseObject(p, out, depth);
case '[':
return jsonParseArray(p, out, depth);
case '"':
return jsonParseString(p, out);
case 't':
case 'f':
case 'n':
return jsonParseLiteral(p, out);
default:
return jsonParseNumber(p, out);
}
}
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp) {
char bytes[4];
size_t count;
if (cp < 0x80) {
bytes[0] = (char)cp;
count = 1;
} else if (cp < 0x800) {
bytes[0] = (char)(0xC0 | (cp >> 6));
bytes[1] = (char)(0x80 | (cp & 0x3F));
count = 2;
} else if (cp < 0x10000) {
bytes[0] = (char)(0xE0 | (cp >> 12));
bytes[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
bytes[2] = (char)(0x80 | (cp & 0x3F));
count = 3;
} else {
bytes[0] = (char)(0xF0 | (cp >> 18));
bytes[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
bytes[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
bytes[3] = (char)(0x80 | (cp & 0x3F));
count = 4;
}
return jsonBufPutBytes(buf, bytes, count);
}
static void jsonSkipWs(JsonParseT *p) {
while (p->cur < p->end) {
char c;
c = *p->cur;
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
p->cur++;
} else {
break;
}
}
}
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
JsonBufT buf;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1) {
return calogFail(result, calogErrArgE, "jsonStringify expects (value)");
}
buf.bytes = NULL;
buf.length = 0;
buf.cap = 0;
status = jsonEncode(&buf, &args[0], 0);
if (status != calogOkE) {
jsonBufFree(&buf);
return calogFail(result, status, "jsonStringify: value is not JSON-serializable");
}
status = calogValueString(result, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
jsonBufFree(&buf);
return status;
}

21
libs/calogJson.h Normal file
View file

@ -0,0 +1,21 @@
// calogJson.h -- calog JSON library.
//
// Bridges JSON text and calog's canonical value/aggregate model, so any engine can parse
// and produce JSON without an engine-specific library:
// jsonParse(text) -> value (object -> map, array -> list, number ->
// int or real, string, true/false, null -> nil)
// jsonStringify(value) -> compact JSON text
// Marshalling is binary-safe; \uXXXX (with surrogate pairs) decodes to UTF-8. Nesting is
// bounded by CALOG_MAX_DEPTH both ways. The natives are INLINE (pure computation, no shared
// state), so there is nothing to shut down.
#ifndef CALOG_JSON_H
#define CALOG_JSON_H
#include "calog.h"
// Register the JSON natives (jsonParse, jsonStringify) on a runtime. Stateless: safe to call
// on any number of runtimes, and there is no matching shutdown.
int32_t calogJsonRegister(CalogT *calog);
#endif

286
libs/calogKv.c Normal file
View file

@ -0,0 +1,286 @@
// calogKv.c -- calog key-value library (see calogKv.h). A process-wide, mutex-guarded array
// of {key bytes, key length, value}. kvSet/kvGet/kvHas/kvDelete/kvKeys are natives over that
// shared store; every stored value is a deep copy, so the store owns pure data.
//
// The registry state is STATIC (not heap) and its bookkeeping is never freed: the five
// natives stay reachable (via any context) right up until calogDestroy tears the broker
// registry down, so freeing the state in calogKvShutdown -- which the contract requires
// BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown
// instead just frees the stored keys + values and empties the array; the small static
// bookkeeping persists for the process (no per-cycle leak), and a post-shutdown native call
// simply sees an empty store.
//
// Function values are refused (calogErrUnsupportedE): storing a CalogFnT would drag script
// callable lifecycle into a data store, so kv sidesteps it entirely and holds data only.
#define _POSIX_C_SOURCE 200809L
#include "calogKv.h"
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define KV_INITIAL_CAP 8
#define KV_GROWTH 2
typedef struct KvEntryT {
char *keyBytes;
int64_t keyLen;
CalogValueT value;
} KvEntryT;
// The store (heap array), guarded by gMapMutex. refCount (guarded by gInitMutex) counts
// registered runtimes so the LAST calogKvShutdown frees the store's contents.
static pthread_mutex_t gMapMutex = PTHREAD_MUTEX_INITIALIZER;
static KvEntryT *gEntries = NULL;
static int32_t gCount = 0;
static int32_t gCap = 0;
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
static int32_t gRefCount = 0;
static bool kvContainsFn(const CalogValueT *value, int32_t depth);
static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvFindLocked(const char *bytes, int64_t length);
static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogKvRegister(CalogT *calog) {
pthread_mutex_lock(&gInitMutex);
gRefCount++;
pthread_mutex_unlock(&gInitMutex);
calogRegisterInline(calog, "kvSet", kvSet, NULL);
calogRegisterInline(calog, "kvGet", kvGet, NULL);
calogRegisterInline(calog, "kvHas", kvHas, NULL);
calogRegisterInline(calog, "kvDelete", kvDelete, NULL);
calogRegisterInline(calog, "kvKeys", kvKeys, NULL);
return calogOkE;
}
void calogKvShutdown(void) {
pthread_mutex_lock(&gInitMutex);
if (gRefCount > 0) {
gRefCount--;
}
if (gRefCount <= 0) {
int32_t index;
pthread_mutex_lock(&gMapMutex);
for (index = 0; index < gCount; index++) {
free(gEntries[index].keyBytes);
calogValueFree(&gEntries[index].value);
}
free(gEntries);
gEntries = NULL;
gCount = 0;
gCap = 0;
pthread_mutex_unlock(&gMapMutex);
}
pthread_mutex_unlock(&gInitMutex);
}
static bool kvContainsFn(const CalogValueT *value, int32_t depth) {
int64_t index;
// kv stores only data (copied by value); a function value -- at the top level OR nested in
// an aggregate -- would retain a CalogFnT into the process-wide store past its owner's life,
// so reject any value that carries one. Too-deep is rejected as unverifiable.
if (depth >= CALOG_MAX_DEPTH) {
return true;
}
if (value->type == calogFnE) {
return true;
}
if (value->type == calogAggE) {
for (index = 0; index < value->as.agg->arrayCount; index++) {
if (kvContainsFn(&value->as.agg->array[index], depth + 1)) {
return true;
}
}
for (index = 0; index < value->as.agg->pairCount; index++) {
if (kvContainsFn(&value->as.agg->pairs[index].key, depth + 1)) {
return true;
}
if (kvContainsFn(&value->as.agg->pairs[index].value, depth + 1)) {
return true;
}
}
}
return false;
}
static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "kvDelete expects (key)");
}
pthread_mutex_lock(&gMapMutex);
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
if (index >= 0) {
free(gEntries[index].keyBytes);
calogValueFree(&gEntries[index].value);
gEntries[index] = gEntries[gCount - 1];
gCount--;
}
pthread_mutex_unlock(&gMapMutex);
return calogOkE;
}
static int32_t kvFindLocked(const char *bytes, int64_t length) {
int32_t index;
for (index = 0; index < gCount; index++) {
if (gEntries[index].keyLen == length && memcmp(gEntries[index].keyBytes, bytes, (size_t)length) == 0) {
return index;
}
}
return -1;
}
static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int32_t index;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "kvGet expects (key)");
}
status = calogOkE;
pthread_mutex_lock(&gMapMutex);
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
if (index >= 0) {
status = calogValueCopy(result, &gEntries[index].value);
}
pthread_mutex_unlock(&gMapMutex);
if (status != calogOkE) {
calogValueFree(result);
return calogFail(result, status, "kvGet: out of memory");
}
return calogOkE;
}
static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "kvHas expects (key)");
}
pthread_mutex_lock(&gMapMutex);
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
pthread_mutex_unlock(&gMapMutex);
calogValueBool(result, index >= 0);
return calogOkE;
}
static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogAggT *agg;
int32_t index;
int32_t status;
(void)args;
(void)argCount;
(void)userData;
calogValueNil(result);
status = calogAggCreate(&agg, calogListE);
if (status != calogOkE) {
return calogFail(result, status, "kvKeys: out of memory");
}
pthread_mutex_lock(&gMapMutex);
for (index = 0; index < gCount; index++) {
CalogValueT key;
status = calogValueString(&key, gEntries[index].keyBytes, gEntries[index].keyLen);
if (status != calogOkE) {
break;
}
status = calogAggPush(agg, &key);
if (status != calogOkE) {
calogValueFree(&key);
break;
}
}
pthread_mutex_unlock(&gMapMutex);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "kvKeys: out of memory");
}
calogValueAgg(result, agg);
return calogOkE;
}
static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogValueT copy;
char *keyBytes;
int64_t keyLen;
int32_t index;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "kvSet expects (key, value)");
}
if (kvContainsFn(&args[1], 0)) {
return calogFail(result, calogErrUnsupportedE, "kv stores data, not functions");
}
status = calogValueCopy(&copy, &args[1]);
if (status != calogOkE) {
calogValueFree(&copy);
return calogFail(result, status, "kvSet: out of memory");
}
keyLen = args[0].as.s.length;
pthread_mutex_lock(&gMapMutex);
index = kvFindLocked(args[0].as.s.bytes, keyLen);
if (index >= 0) {
// Replace: drop the old value, keep the existing key bytes.
calogValueFree(&gEntries[index].value);
gEntries[index].value = copy;
pthread_mutex_unlock(&gMapMutex);
return calogOkE;
}
if (gCount == gCap) {
int32_t newCap;
KvEntryT *grown;
newCap = (gCap == 0) ? KV_INITIAL_CAP : gCap * KV_GROWTH;
grown = (KvEntryT *)realloc(gEntries, (size_t)newCap * sizeof(KvEntryT));
if (grown == NULL) {
pthread_mutex_unlock(&gMapMutex);
calogValueFree(&copy);
return calogFail(result, calogErrOomE, "kvSet: out of memory");
}
gEntries = grown;
gCap = newCap;
}
keyBytes = (char *)malloc((size_t)keyLen + 1);
if (keyBytes == NULL) {
pthread_mutex_unlock(&gMapMutex);
calogValueFree(&copy);
return calogFail(result, calogErrOomE, "kvSet: out of memory");
}
if (keyLen > 0) {
memcpy(keyBytes, args[0].as.s.bytes, (size_t)keyLen);
}
keyBytes[keyLen] = '\0';
gEntries[gCount].keyBytes = keyBytes;
gEntries[gCount].keyLen = keyLen;
gEntries[gCount].value = copy;
gCount++;
pthread_mutex_unlock(&gMapMutex);
return calogOkE;
}

30
libs/calogKv.h Normal file
View file

@ -0,0 +1,30 @@
// calogKv.h -- calog key-value library: a process-wide, thread-safe data store.
//
// Registers natives so any script, on any engine, can stash and fetch values under a
// string key in one shared store:
// kvSet(key, value) store a DEEP COPY of value under key (replaces any existing) -> nil
// kvGet(key) -> a deep copy of the stored value, or nil if the key is absent
// kvHas(key) -> true if the key is present, else false
// kvDelete(key) remove the key (no error if it is absent) -> nil
// kvKeys() -> a list of the stored keys (each a string)
//
// The store holds DATA ONLY: kvSet rejects a function value (calogFnE) so no script
// callable's lifecycle leaks into the shared store -- kv keeps only values copied by
// value. Keys are binary-safe: they are compared by bytes + length, so embedded NULs are
// honored. The registry is process-wide and reference-counted across runtimes.
#ifndef CALOG_KV_H
#define CALOG_KV_H
#include "calog.h"
// Register the kv natives on a runtime. Idempotent across runtimes (shared registry).
int32_t calogKvRegister(CalogT *calog);
// Free every stored key + value (once the last registered runtime unregisters). Like
// calogExportShutdown, call this BEFORE calogDestroy. The static registry bookkeeping is
// intentionally never freed -- the natives stay callable right up until calogDestroy tears
// the broker down -- so a native call after shutdown is safe (it just sees an empty store).
void calogKvShutdown(void);
#endif

229
libs/calogPubsub.c Normal file
View file

@ -0,0 +1,229 @@
// calogPubsub.c -- calog publish/subscribe library (see calogPubsub.h). A process-wide,
// mutex-guarded list of {int64 id, topic bytes, topic length, callback}. subscribe/
// unsubscribe/publish are natives over that shared list; subscribe retains the callback
// function value, and publish delivers a deep copy of the message to every matching
// subscriber SYNCHRONOUSLY.
//
// The registry state is STATIC (not heap) and its bookkeeping is never freed: the three
// natives stay reachable (via any context) right up until calogDestroy tears the broker
// registry down, so freeing the state in calogPubsubShutdown -- which the contract requires
// BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown
// instead releases every subscribed callback and empties the list; the small static
// bookkeeping persists for the process (no per-cycle leak), and a post-shutdown publish
// simply finds no subscribers.
//
// Delivery discipline: publish collects (and retains) the matching callbacks under the lock,
// UNLOCKS, then invokes each -- the mutex is NEVER held across calogFnInvoke, and the
// retained reference keeps a concurrently-unsubscribed callback alive across the call. Each
// callback marshals to its owner context's thread; a subscriber that publishes back onto a
// topic it is on can deadlock (delivery is synchronous), so keep publish graphs acyclic.
#define _POSIX_C_SOURCE 200809L
#include "calogPubsub.h"
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define PUBSUB_INITIAL_CAP 8
#define PUBSUB_GROWTH 2
typedef struct SubEntryT {
int64_t id;
char *topic;
int64_t topicLen;
CalogFnT *callback;
} SubEntryT;
// The list (heap array), guarded by gListMutex. gNextId hands out monotonically increasing
// subscription ids. refCount (guarded by gInitMutex) counts registered runtimes so the LAST
// calogPubsubShutdown releases the callbacks and empties the list.
static pthread_mutex_t gListMutex = PTHREAD_MUTEX_INITIALIZER;
static SubEntryT *gEntries = NULL;
static int32_t gCount = 0;
static int32_t gCap = 0;
static int64_t gNextId = 1;
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
static int32_t gRefCount = 0;
static int32_t pubsubFindByIdLocked(int64_t id);
static int32_t pubsubPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t pubsubSubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t pubsubUnsubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogPubsubRegister(CalogT *calog) {
pthread_mutex_lock(&gInitMutex);
gRefCount++;
pthread_mutex_unlock(&gInitMutex);
calogRegisterInline(calog, "subscribe", pubsubSubscribe, NULL);
calogRegisterInline(calog, "unsubscribe", pubsubUnsubscribe, NULL);
calogRegisterInline(calog, "publish", pubsubPublish, NULL);
return calogOkE;
}
void calogPubsubShutdown(void) {
pthread_mutex_lock(&gInitMutex);
if (gRefCount > 0) {
gRefCount--;
}
if (gRefCount <= 0) {
int32_t index;
pthread_mutex_lock(&gListMutex);
for (index = 0; index < gCount; index++) {
free(gEntries[index].topic);
calogFnRelease(gEntries[index].callback);
}
free(gEntries);
gEntries = NULL;
gCount = 0;
gCap = 0;
pthread_mutex_unlock(&gListMutex);
}
pthread_mutex_unlock(&gInitMutex);
}
static int32_t pubsubFindByIdLocked(int64_t id) {
int32_t index;
for (index = 0; index < gCount; index++) {
if (gEntries[index].id == id) {
return index;
}
}
return -1;
}
static int32_t pubsubPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
CalogFnT **collected;
int64_t topicLen;
int32_t delivered;
int32_t matched;
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "publish expects (topic, message)");
}
topicLen = args[0].as.s.length;
collected = NULL;
matched = 0;
delivered = 0;
pthread_mutex_lock(&gListMutex);
if (gCount > 0) {
collected = (CalogFnT **)malloc((size_t)gCount * sizeof(CalogFnT *));
if (collected == NULL) {
pthread_mutex_unlock(&gListMutex);
return calogFail(result, calogErrOomE, "publish: out of memory");
}
for (index = 0; index < gCount; index++) {
if (gEntries[index].topicLen == topicLen && memcmp(gEntries[index].topic, args[0].as.s.bytes, (size_t)topicLen) == 0) {
// Retain across the (unlocked) delivery so a concurrent unsubscribe cannot free it.
calogFnRetain(gEntries[index].callback);
collected[matched] = gEntries[index].callback;
matched++;
}
}
}
pthread_mutex_unlock(&gListMutex);
// Deliver OUTSIDE the lock: each callback marshals to its owner context's thread and may
// re-enter subscribe/unsubscribe/publish. The mutex is never held across calogFnInvoke.
for (index = 0; index < matched; index++) {
CalogValueT messageCopy;
CalogValueT callResult;
int32_t status;
status = calogValueCopy(&messageCopy, &args[1]);
if (status != calogOkE) {
calogValueFree(&messageCopy);
calogFnRelease(collected[index]);
continue;
}
calogValueNil(&callResult);
status = calogFnInvoke(collected[index], &messageCopy, 1, &callResult);
calogValueFree(&callResult);
calogValueFree(&messageCopy);
calogFnRelease(collected[index]);
// A subscriber whose owner context is gone was not actually invoked, so it is not
// counted (calogErrDeadE from the marshalled path, calogErrNotFoundE from the direct).
if (status != calogErrDeadE && status != calogErrNotFoundE) {
delivered++;
}
}
free(collected);
calogValueInt(result, (int64_t)delivered);
return calogOkE;
}
static int32_t pubsubSubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
char *topicCopy;
int64_t id;
int64_t topicLen;
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogFnE) {
return calogFail(result, calogErrArgE, "subscribe expects (topic, function)");
}
topicLen = args[0].as.s.length;
topicCopy = (char *)malloc((size_t)topicLen + 1);
if (topicCopy == NULL) {
return calogFail(result, calogErrOomE, "subscribe: out of memory");
}
memcpy(topicCopy, args[0].as.s.bytes, (size_t)topicLen);
topicCopy[topicLen] = '\0';
pthread_mutex_lock(&gListMutex);
if (gCount == gCap) {
int32_t newCap;
SubEntryT *grown;
newCap = (gCap == 0) ? PUBSUB_INITIAL_CAP : gCap * PUBSUB_GROWTH;
grown = (SubEntryT *)realloc(gEntries, (size_t)newCap * sizeof(SubEntryT));
if (grown == NULL) {
pthread_mutex_unlock(&gListMutex);
free(topicCopy);
return calogFail(result, calogErrOomE, "subscribe: out of memory");
}
gEntries = grown;
gCap = newCap;
}
id = gNextId;
gNextId++;
calogFnRetain(args[1].as.fn);
gEntries[gCount].id = id;
gEntries[gCount].topic = topicCopy;
gEntries[gCount].topicLen = topicLen;
gEntries[gCount].callback = args[1].as.fn;
gCount++;
pthread_mutex_unlock(&gListMutex);
calogValueInt(result, id);
return calogOkE;
}
static int32_t pubsubUnsubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t id;
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 1 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "unsubscribe expects (id)");
}
id = (args[0].type == calogIntE) ? args[0].as.i : (int64_t)args[0].as.r;
pthread_mutex_lock(&gListMutex);
index = pubsubFindByIdLocked(id);
if (index >= 0) {
free(gEntries[index].topic);
calogFnRelease(gEntries[index].callback);
gEntries[index] = gEntries[gCount - 1];
gCount--;
}
pthread_mutex_unlock(&gListMutex);
return calogOkE;
}

41
libs/calogPubsub.h Normal file
View file

@ -0,0 +1,41 @@
// calogPubsub.h -- calog publish/subscribe library: deliver a message to every subscriber
// of a topic, across contexts and engines.
//
// Registers natives so a script can broadcast to any number of listeners by topic name:
// subscribe(topic, fn) -> id register fn to receive messages published on topic
// unsubscribe(id) -> nil drop the subscription with that id
// publish(topic, msg) -> count deliver a copy of msg to every subscriber of topic,
// returning how many subscribers were invoked
//
// A subscriber is an ordinary calog function value, so each delivery runs in the
// subscriber's OWN context/thread (marshalled like any callable) and receives its own deep
// copy of the message -- publishing binary-safe data or nested aggregates is fine. Topics are
// matched by exact bytes (binary-safe), so a topic may itself contain embedded NULs.
//
// Delivery is SYNCHRONOUS: publish does not return until every matching subscriber has run.
// Because a subscriber runs on its owner context's thread, a subscriber that (directly or
// transitively) publishes back onto a topic it is itself subscribed to can DEADLOCK -- the
// publishing thread blocks in the callback, which cannot complete until the first publish
// returns. Keep publish graphs acyclic. A publish after a subscriber's owner context is gone
// simply skips that subscriber and does not count it.
//
// The registry is process-wide and reference-counted across runtimes.
#ifndef CALOG_PUBSUB_H
#define CALOG_PUBSUB_H
#include "calog.h"
// Register the pubsub natives (subscribe, unsubscribe, publish) on a runtime. Idempotent
// across runtimes (shared registry).
int32_t calogPubsubRegister(CalogT *calog);
// Release every subscribed function (once the last registered runtime unregisters). Like the
// export library, call this while the subscribing contexts are still ALIVE -- before you
// close them and before calogDestroy -- because a subscription is a live reference into its
// owner's interpreter; releasing it after that context is gone would touch freed memory. The
// static registry bookkeeping itself is intentionally never freed (the natives stay callable
// until calogDestroy), so this is safe to call and re-register across runtimes.
void calogPubsubShutdown(void);
#endif

91
libs/calogTime.c Normal file
View file

@ -0,0 +1,91 @@
// calogTime.c -- calog time library (see calogTime.h). Three stateless natives over the
// POSIX clocks: a wall-clock read, a monotonic read, and a millisecond sleep. No shared
// state and no timers/threads -- each native is a pure function of its arguments (or a
// plain block of the calling context thread).
#define _POSIX_C_SOURCE 200809L
#include "calogTime.h"
#include <errno.h>
#include <time.h>
static int32_t timeMonotonicNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t timeNowNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t timeSleepNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static double timeToSeconds(const struct timespec *ts);
int32_t calogTimeRegister(CalogT *calog) {
calogRegisterInline(calog, "timeNow", timeNowNative, NULL);
calogRegisterInline(calog, "timeMonotonic", timeMonotonicNative, NULL);
calogRegisterInline(calog, "timeSleep", timeSleepNative, NULL);
return calogOkE;
}
static int32_t timeMonotonicNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct timespec ts;
(void)args;
(void)userData;
calogValueNil(result);
if (argCount != 0) {
return calogFail(result, calogErrArgE, "timeMonotonic expects ()");
}
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return calogFail(result, calogErrUnsupportedE, "timeMonotonic: clock_gettime failed");
}
calogValueReal(result, timeToSeconds(&ts));
return calogOkE;
}
static int32_t timeNowNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct timespec ts;
(void)args;
(void)userData;
calogValueNil(result);
if (argCount != 0) {
return calogFail(result, calogErrArgE, "timeNow expects ()");
}
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {
return calogFail(result, calogErrUnsupportedE, "timeNow: clock_gettime failed");
}
calogValueReal(result, timeToSeconds(&ts));
return calogOkE;
}
static int32_t timeSleepNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct timespec request;
struct timespec remaining;
double ms;
int64_t nanos;
(void)userData;
calogValueNil(result);
if (argCount != 1 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "timeSleep expects (milliseconds)");
}
ms = (args[0].type == calogIntE) ? (double)args[0].as.i : args[0].as.r;
if (!(ms >= 0.0)) {
return calogFail(result, calogErrRangeE, "timeSleep: milliseconds must be non-negative");
}
nanos = (int64_t)(ms * 1000000.0);
request.tv_sec = (time_t)(nanos / 1000000000);
request.tv_nsec = (long)(nanos % 1000000000);
while (nanosleep(&request, &remaining) != 0) {
if (errno != EINTR) {
return calogFail(result, calogErrUnsupportedE, "timeSleep: nanosleep failed");
}
request = remaining;
}
return calogOkE;
}
static double timeToSeconds(const struct timespec *ts) {
return (double)ts->tv_sec + (double)ts->tv_nsec / 1000000000.0;
}

20
libs/calogTime.h Normal file
View file

@ -0,0 +1,20 @@
// calogTime.h -- calog time library.
//
// A tiny, stateless clock + sleep bridge, so any engine can read the wall-clock and
// monotonic clocks and block for a while without an engine-specific library:
// timeNow() -> real (epoch seconds, fractional; CLOCK_REALTIME)
// timeMonotonic() -> real (seconds from an unspecified origin; CLOCK_MONOTONIC)
// timeSleep(ms) -> nil (block the calling context for ms milliseconds)
// The natives are INLINE (pure clock reads / a plain blocking sleep, no shared state, no
// timers or threads), so there is nothing to shut down.
#ifndef CALOG_TIME_H
#define CALOG_TIME_H
#include "calog.h"
// Register the time natives (timeNow, timeMonotonic, timeSleep) on a runtime. Stateless:
// safe to call on any number of runtimes, and there is no matching shutdown.
int32_t calogTimeRegister(CalogT *calog);
#endif

350
libs/calogTimer.c Normal file
View file

@ -0,0 +1,350 @@
// calogTimer.c -- calog timer library (see calogTimer.h). ONE background thread owns a
// mutex-guarded list of timers and, for each one that comes due, retains its callback, drops
// the list lock, and invokes it (which marshals to the callback's owning context thread). The
// list is scheduled on CLOCK_MONOTONIC; the thread sleeps on a condition variable (given the
// MONOTONIC clock via a condattr) until the earliest nextFire, or until it is signalled when a
// timer is added or cancelled.
//
// The registry state is STATIC and its small bookkeeping is never freed: the three natives
// stay reachable (via any context) right up until calogDestroy tears the broker registry down,
// so freeing the state in calogTimerShutdown -- which the contract requires BEFORE calogDestroy
// -- would leave those natives dereferencing freed memory. Shutdown instead stops the thread,
// releases every remaining callback, and empties the list; the mutexes/counters persist for the
// process (no per-cycle leak), so the library re-registers cleanly across runtimes.
//
// Locking discipline: the list mutex is NEVER held across calogFnInvoke (a blocking, cross-
// thread call) -- the callback is retained, the lock dropped, then the callback invoked and
// released. calogFnRelease is only ever held under the list mutex to post a non-blocking
// finalize (as in calogExport); it never re-enters the timer library.
#define _POSIX_C_SOURCE 200809L
#include "calogTimer.h"
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
#define TIMER_INITIAL_CAP 8
#define TIMER_GROWTH 2
// One scheduled timer. intervalNs == 0 marks a one-shot; a periodic timer keeps a non-zero
// interval. A cancelled timer is tombstoned here and pruned by the thread at the top of its
// loop, which keeps cancellation safe against a fire that is already in flight.
typedef struct TimerEntryT {
int64_t id;
CalogFnT *callback;
int64_t nextFireMonoNs;
int64_t intervalNs;
bool cancelled;
} TimerEntryT;
// The timer list (heap array) plus its background thread, guarded by gTimerMutex/gTimerCond.
// refCount (guarded by gInitMutex) counts registered runtimes so the LAST calogTimerShutdown
// stops the thread and frees the list. gTimerCond is initialised (with CLOCK_MONOTONIC) when
// the thread starts and destroyed when it stops, so every cond operation is guarded by
// gThreadStarted.
static pthread_mutex_t gTimerMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t gTimerCond;
static TimerEntryT *gTimers = NULL;
static int32_t gCount = 0;
static int32_t gCap = 0;
static int64_t gNextId = 1;
static pthread_t gThread;
static bool gThreadStarted = false;
static bool gShutdown = false;
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
static int32_t gRefCount = 0;
static int32_t timerAfterNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t timerCancelNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t timerEnsureThreadLocked(void);
static int32_t timerEveryNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t timerFindByIdLocked(int64_t id);
static int64_t timerNowNs(void);
static void timerPruneLocked(void);
static int32_t timerSchedule(CalogValueT *args, int32_t argCount, CalogValueT *result, bool periodic);
static void *timerThreadMain(void *arg);
int32_t calogTimerRegister(CalogT *calog) {
pthread_mutex_lock(&gInitMutex);
gRefCount++;
pthread_mutex_unlock(&gInitMutex);
calogRegisterInline(calog, "timerAfter", timerAfterNative, NULL);
calogRegisterInline(calog, "timerEvery", timerEveryNative, NULL);
calogRegisterInline(calog, "timerCancel", timerCancelNative, NULL);
return calogOkE;
}
void calogTimerShutdown(void) {
pthread_t threadHandle;
bool joinThread;
int32_t index;
joinThread = false;
pthread_mutex_lock(&gInitMutex);
if (gRefCount > 0) {
gRefCount--;
}
if (gRefCount > 0) {
pthread_mutex_unlock(&gInitMutex);
return;
}
// Last runtime is gone: stop the thread (if it ever started), then release every remaining
// callback and free the list. Contexts are still alive here, so a callback release routes a
// finalize to its owner thread just like calogExport.
pthread_mutex_lock(&gTimerMutex);
gShutdown = true;
threadHandle = gThread;
if (gThreadStarted) {
joinThread = true;
pthread_cond_signal(&gTimerCond);
}
pthread_mutex_unlock(&gTimerMutex);
if (joinThread) {
pthread_join(threadHandle, NULL);
}
pthread_mutex_lock(&gTimerMutex);
for (index = 0; index < gCount; index++) {
calogFnRelease(gTimers[index].callback);
}
free(gTimers);
gTimers = NULL;
gCount = 0;
gCap = 0;
if (joinThread) {
pthread_cond_destroy(&gTimerCond);
}
gThreadStarted = false;
gShutdown = false;
pthread_mutex_unlock(&gTimerMutex);
pthread_mutex_unlock(&gInitMutex);
}
static int32_t timerAfterNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
return timerSchedule(args, argCount, result, false);
}
static int32_t timerCancelNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int32_t index;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogIntE) {
return calogFail(result, calogErrArgE, "timerCancel expects (id)");
}
pthread_mutex_lock(&gTimerMutex);
index = timerFindByIdLocked(args[0].as.i);
if (index >= 0) {
// Tombstone it; the thread prunes it (and stops firing it) at the top of its loop.
gTimers[index].cancelled = true;
pthread_cond_signal(&gTimerCond);
}
pthread_mutex_unlock(&gTimerMutex);
return calogOkE;
}
static int32_t timerEnsureThreadLocked(void) {
pthread_condattr_t attr;
// Caller holds gTimerMutex. Start the single background thread lazily, on the first timer.
if (gThreadStarted) {
return calogOkE;
}
if (pthread_condattr_init(&attr) != 0) {
return calogErrUnsupportedE;
}
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
if (pthread_cond_init(&gTimerCond, &attr) != 0) {
pthread_condattr_destroy(&attr);
return calogErrUnsupportedE;
}
pthread_condattr_destroy(&attr);
gShutdown = false;
if (pthread_create(&gThread, NULL, timerThreadMain, NULL) != 0) {
pthread_cond_destroy(&gTimerCond);
return calogErrUnsupportedE;
}
gThreadStarted = true;
return calogOkE;
}
static int32_t timerEveryNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
return timerSchedule(args, argCount, result, true);
}
static int32_t timerFindByIdLocked(int64_t id) {
int32_t index;
for (index = 0; index < gCount; index++) {
if (gTimers[index].id == id) {
return index;
}
}
return -1;
}
static int64_t timerNowNs(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000000 + (int64_t)ts.tv_nsec;
}
static void timerPruneLocked(void) {
int32_t index;
index = 0;
while (index < gCount) {
if (gTimers[index].cancelled) {
calogFnRelease(gTimers[index].callback);
gTimers[index] = gTimers[gCount - 1];
gCount--;
} else {
index++;
}
}
}
static int32_t timerSchedule(CalogValueT *args, int32_t argCount, CalogValueT *result, bool periodic) {
const char *label;
double ms;
int64_t delayNs;
int64_t id;
int32_t status;
label = periodic ? "timerEvery expects (ms, function)" : "timerAfter expects (ms, function)";
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || args[1].type != calogFnE) {
return calogFail(result, calogErrArgE, label);
}
ms = (args[0].type == calogIntE) ? (double)args[0].as.i : args[0].as.r;
// Bound ms so ms*1e6 stays well within int64 nanoseconds. This also rejects +Inf and NaN
// (both fail the comparison); the cast below is otherwise undefined once the product
// exceeds INT64_MAX. 9.0e12 ms is ~285 years -- far beyond any real timer.
if (!(ms >= 0.0 && ms <= 9.0e12)) {
return calogFail(result, calogErrRangeE, "timer: milliseconds out of range");
}
delayNs = (int64_t)(ms * 1000000.0);
pthread_mutex_lock(&gTimerMutex);
if (gCount == gCap) {
int32_t newCap;
TimerEntryT *grown;
newCap = (gCap == 0) ? TIMER_INITIAL_CAP : gCap * TIMER_GROWTH;
grown = (TimerEntryT *)realloc(gTimers, (size_t)newCap * sizeof(TimerEntryT));
if (grown == NULL) {
pthread_mutex_unlock(&gTimerMutex);
return calogFail(result, calogErrOomE, "timer: out of memory");
}
gTimers = grown;
gCap = newCap;
}
status = timerEnsureThreadLocked();
if (status != calogOkE) {
pthread_mutex_unlock(&gTimerMutex);
return calogFail(result, status, "timer: could not start the timer thread");
}
id = gNextId++;
// Retain the callback for as long as the list holds it; released on cancel/fire/shutdown.
calogFnRetain(args[1].as.fn);
gTimers[gCount].id = id;
gTimers[gCount].callback = args[1].as.fn;
gTimers[gCount].nextFireMonoNs = timerNowNs() + delayNs;
gTimers[gCount].intervalNs = periodic ? ((delayNs > 0) ? delayNs : 1) : 0;
gTimers[gCount].cancelled = false;
gCount++;
pthread_cond_signal(&gTimerCond);
pthread_mutex_unlock(&gTimerMutex);
calogValueInt(result, id);
return calogOkE;
}
static void *timerThreadMain(void *arg) {
(void)arg;
pthread_mutex_lock(&gTimerMutex);
for (;;) {
int64_t nowNs;
int32_t index;
int32_t minIndex;
// Drop tombstoned timers first, so the scan below only sees live entries.
timerPruneLocked();
if (gShutdown) {
break;
}
if (gCount == 0) {
pthread_cond_wait(&gTimerCond, &gTimerMutex);
continue;
}
nowNs = timerNowNs();
minIndex = 0;
for (index = 1; index < gCount; index++) {
if (gTimers[index].nextFireMonoNs < gTimers[minIndex].nextFireMonoNs) {
minIndex = index;
}
}
if (gTimers[minIndex].nextFireMonoNs > nowNs) {
struct timespec target;
int64_t fireAt;
fireAt = gTimers[minIndex].nextFireMonoNs;
target.tv_sec = (time_t)(fireAt / 1000000000);
target.tv_nsec = (long)(fireAt % 1000000000);
pthread_cond_timedwait(&gTimerCond, &gTimerMutex, &target);
continue;
}
{
CalogFnT *cb;
CalogValueT res;
int64_t id;
int32_t status;
bool oneShot;
cb = gTimers[minIndex].callback;
id = gTimers[minIndex].id;
oneShot = (gTimers[minIndex].intervalNs == 0);
// Retain across the unlocked invoke so a concurrent cancel/shutdown can't free it.
calogFnRetain(cb);
if (oneShot) {
gTimers[minIndex].cancelled = true;
} else {
gTimers[minIndex].nextFireMonoNs += gTimers[minIndex].intervalNs;
if (gTimers[minIndex].nextFireMonoNs <= nowNs) {
// Fell behind (a slow callback): resync rather than fire in a burst.
gTimers[minIndex].nextFireMonoNs = nowNs + gTimers[minIndex].intervalNs;
}
}
pthread_mutex_unlock(&gTimerMutex);
status = calogFnInvoke(cb, NULL, 0, &res);
calogValueFree(&res);
if (status == calogErrDeadE) {
// The owning context is gone: auto-cancel the timer (find it again by id, since
// the list may have changed while unlocked).
pthread_mutex_lock(&gTimerMutex);
index = timerFindByIdLocked(id);
if (index >= 0) {
gTimers[index].cancelled = true;
}
pthread_mutex_unlock(&gTimerMutex);
}
calogFnRelease(cb);
pthread_mutex_lock(&gTimerMutex);
}
}
pthread_mutex_unlock(&gTimerMutex);
return NULL;
}

29
libs/calogTimer.h Normal file
View file

@ -0,0 +1,29 @@
// calogTimer.h -- calog timer library.
//
// One background thread drives every timer and invokes each script callback on its OWN
// owning context (marshalled like any calog function value, so the callback runs on the
// thread that created the timer). Three natives:
// timerAfter(ms, function) -> id fire the callback once, ms milliseconds from now
// timerEvery(ms, function) -> id fire the callback every ms milliseconds
// timerCancel(id) -> nil stop a pending or repeating timer (id from the above)
//
// If a timer's owning context is gone by the time it fires, the invoke fails cleanly and the
// timer auto-cancels itself. ms may be an integer or a real and must be non-negative. The
// registry (and its background thread) is process-wide and reference-counted across runtimes.
#ifndef CALOG_TIMER_H
#define CALOG_TIMER_H
#include "calog.h"
// Register the timer natives (timerAfter, timerEvery, timerCancel) on a runtime. The shared
// registry is reference-counted, so this is safe to call on any number of runtimes.
int32_t calogTimerRegister(CalogT *calog);
// Release every still-pending timer's callback and (once the last registered runtime
// unregisters) stop the background thread and free the timer list. Like calogExport, call
// this while the timer-owning contexts are still ALIVE -- before you close them and before
// calogDestroy -- because a pending callback is a live reference into its owner's interpreter.
void calogTimerShutdown(void);
#endif

165
tests/testCrypto.c Normal file
View file

@ -0,0 +1,165 @@
// testCrypto.c -- exercises the crypto library: SHA-256/SHA-1 hashes, HMAC-SHA-256, base64
// and hex codecs (with round-trips and binary safety), random bytes, and UUIDs, driven from
// a Lua context.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogCrypto.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 16
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogCryptoRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx,
"report(1, #hashSha256('abc'))\n" // 64 hex chars
"report(2, hashSha256('abc') == 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' and 1 or 0)\n" // known vector
"report(3, hashSha1('abc') == 'a9993e364706816aba3e25717850c26c9cd0d89d' and 1 or 0)\n" // known vector
"report(4, hmacSha256('key', 'The quick brown fox jumps over the lazy dog') == 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8' and 1 or 0)\n" // known vector
"report(5, base64Encode('abc') == 'YWJj' and 1 or 0)\n" // known encoding
"report(6, base64Decode(base64Encode('hello')) == 'hello' and 1 or 0)\n" // round-trip, one pad byte
"report(7, hexEncode('abc') == '616263' and 1 or 0)\n" // known encoding
"report(8, hexDecode(hexEncode('Hello!')) == 'Hello!' and 1 or 0)\n" // round-trip
"report(9, #randomBytes(16))\n" // 16 bytes
"local a = uuid()\n"
"local b = uuid()\n"
"report(10, (#a == 36 and #b == 36 and a ~= b) and 1 or 0)\n" // distinct, well-formed
"report(11, base64Decode(base64Encode('a')) == 'a' and 1 or 0)\n" // round-trip, two pad bytes
"report(12, hexEncode('a\\0b') == '610062' and 1 or 0)\n" // binary-safe (embedded NUL)
"local ok = pcall(function() hexDecode('xyz') end)\n"
"report(13, ok and 0 or 1)\n" // invalid hex is catchable
"report(14, base64Decode('YQ==\\n') == 'a' and 1 or 0)\n" // trailing whitespace does not add spurious NULs
"done()");
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 64, "sha256 hex digest is 64 chars");
CHECK(atomic_load(&results[2]) == 1, "sha256('abc') matches the known vector");
CHECK(atomic_load(&results[3]) == 1, "sha1('abc') matches the known vector");
CHECK(atomic_load(&results[4]) == 1, "hmacSha256 matches the known vector");
CHECK(atomic_load(&results[5]) == 1, "base64Encode('abc') is YWJj");
CHECK(atomic_load(&results[6]) == 1, "base64 round-trips a padded value");
CHECK(atomic_load(&results[7]) == 1, "hexEncode('abc') is 616263");
CHECK(atomic_load(&results[8]) == 1, "hex round-trips a value");
CHECK(atomic_load(&results[9]) == 16, "randomBytes(16) yields 16 bytes");
CHECK(atomic_load(&results[10]) == 1, "two uuids are distinct and 36 chars");
CHECK(atomic_load(&results[11]) == 1, "base64 round-trips a double-padded value");
CHECK(atomic_load(&results[12]) == 1, "hexEncode is binary-safe over embedded NULs");
CHECK(atomic_load(&results[13]) == 1, "invalid hex raises a catchable error");
CHECK(atomic_load(&results[14]) == 1, "base64Decode trims trailing whitespace without spurious trailing NULs");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

178
tests/testFs.c Normal file
View file

@ -0,0 +1,178 @@
// testFs.c -- exercises the filesystem library: write/read round-trip (binary-safe), append,
// exists transitions, stat (size + file/dir kind), directory listing, remove, and a catchable
// error, all driven from a Lua context in a private temp directory.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogFs.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
char script[4096];
char dir[64];
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogFsRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
// A private per-process temp dir keeps the test hermetic and re-runnable.
snprintf(dir, sizeof(dir), "/tmp/calogFsTest_%ld", (long)getpid());
snprintf(script, sizeof(script),
"local dir = '%s'\n"
"fsMkdir(dir)\n"
"local path = dir .. '/data.bin'\n"
"local data = 'hello\\0world'\n" // 11 bytes, embedded NUL
"fsWrite(path, data)\n"
"report(1, fsExists(path) and 1 or 0)\n" // 1, the file now exists
"local back = fsRead(path)\n"
"report(2, back == data and 1 or 0)\n" // 1, binary-safe round-trip
"report(3, #back)\n" // 11, whole file read by size
"local st = fsStat(path)\n"
"report(4, st.size)\n" // 11, stat size
"report(5, (st.isFile and not st.isDir) and 1 or 0)\n" // 1, it is a regular file
"fsAppend(path, '!')\n"
"report(6, #fsRead(path))\n" // 12, append added a byte
"local found = 0\n"
"for _, name in ipairs(fsList(dir)) do\n"
" if name == 'data.bin' then found = 1 end\n"
"end\n"
"report(7, found)\n" // 1, listing sees the file
"report(8, (fsStat(dir).isDir and not fsStat(dir).isFile) and 1 or 0)\n" // 1, directory stat
"local ok = pcall(function() fsRead(dir .. '/nope.dat') end)\n"
"report(9, ok and 0 or 1)\n" // 1, missing read is catchable
"fsRemove(path)\n"
"report(10, fsExists(path) and 0 or 1)\n" // 1, exists went false
"report(11, fsStat(path) == nil and 1 or 0)\n" // 1, stat of a missing path is nil
"fsRemove(dir)\n"
"done()", dir);
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx, script);
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 1, "fsWrite then fsExists sees the new file");
CHECK(atomic_load(&results[2]) == 1, "fsRead round-trips binary data with an embedded NUL");
CHECK(atomic_load(&results[3]) == 11, "fsRead returns the whole file by byte length");
CHECK(atomic_load(&results[4]) == 11, "fsStat.size matches the bytes written");
CHECK(atomic_load(&results[5]) == 1, "fsStat reports a regular file (isFile, not isDir)");
CHECK(atomic_load(&results[6]) == 12, "fsAppend grew the file by one byte");
CHECK(atomic_load(&results[7]) == 1, "fsList contains the written file");
CHECK(atomic_load(&results[8]) == 1, "fsStat reports a directory (isDir, not isFile)");
CHECK(atomic_load(&results[9]) == 1, "reading a missing file raises a catchable error");
CHECK(atomic_load(&results[10]) == 1, "fsRemove deletes the file (fsExists transitions to false)");
CHECK(atomic_load(&results[11]) == 1, "fsStat of a missing path returns nil");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

233
tests/testHttp.c Normal file
View file

@ -0,0 +1,233 @@
// testHttp.c -- exercises the HTTP client library. A tiny in-test HTTP/1.1 server runs on its
// own thread (bind an ephemeral 127.0.0.1 port, accept one connection, reply with a fixed
// Content-Length response), and a Lua context httpGet()s it and reports the status, body, and a
// lowercased header. Also covers the connect-refused and bad-scheme error paths. Kept to
// plain http:// so no TLS server is needed, though the binary still links OpenSSL because the
// library references it.
#define _GNU_SOURCE
#include "calog.h"
#include "calogHttp.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int gListenFd = -1;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static void *serverThread(void *arg);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
static void *serverThread(void *arg) {
static const char ok[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
// A hostile chunked response: a chunk size that saturates size_t. A correct client rejects
// it (a catchable error); a broken one loops forever in the dechunker.
static const char evil[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nffffffffffffffff\r\nx\r\n0\r\n\r\n";
char buf[2048];
int n;
(void)arg;
// Serve two connections in order: the well-formed response, then the hostile one. Each
// consumes (part of) the request, replies, and closes so the client's read-to-EOF ends.
for (n = 0; n < 2; n++) {
int cfd;
cfd = accept(gListenFd, NULL, NULL);
if (cfd < 0) {
break;
}
{
ssize_t received;
const char *response;
size_t length;
received = recv(cfd, buf, sizeof(buf), 0);
(void)received;
response = (n == 0) ? ok : evil;
length = (n == 0) ? (sizeof(ok) - 1) : (sizeof(evil) - 1);
(void)send(cfd, response, length, MSG_NOSIGNAL);
close(cfd);
}
}
return NULL;
}
int main(void) {
CalogContextT *ctx;
pthread_t server;
struct sockaddr_in addr;
socklen_t addrLen;
char script[512];
int port;
int yes;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogHttpRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
// In-test HTTP/1.1 server on an ephemeral loopback port.
yes = 1;
gListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (gListenFd < 0) {
printf("socket failed\n");
return 1;
}
setsockopt(gListenFd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(gListenFd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
printf("bind failed\n");
return 1;
}
if (listen(gListenFd, 1) != 0) {
printf("listen failed\n");
return 1;
}
addrLen = sizeof(addr);
if (getsockname(gListenFd, (struct sockaddr *)&addr, &addrLen) != 0) {
printf("getsockname failed\n");
return 1;
}
port = (int)ntohs(addr.sin_port);
pthread_create(&server, NULL, serverThread, NULL);
snprintf(script, sizeof(script),
"local r = httpGet('http://127.0.0.1:%d/')\n"
"report(1, r.status)\n"
"report(2, r.body == 'hello' and 1 or 0)\n"
"report(3, r.headers['content-length'] == '5' and 1 or 0)\n"
"local okc = pcall(function() return httpGet('http://127.0.0.1:%d/') end)\n"
"report(6, okc and 0 or 1)\n" // hostile chunk size -> catchable error, not a hang
"local ok = pcall(function() httpGet('http://127.0.0.1:1/') end)\n"
"report(4, ok and 0 or 1)\n"
"local ok2 = pcall(function() httpGet('ftp://example/') end)\n"
"report(5, ok2 and 0 or 1)\n"
"done()", port, port);
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx, script);
pumpUntilDone(1);
pthread_join(server, NULL);
close(gListenFd);
CHECK(atomic_load(&results[1]) == 200, "httpGet returns status 200 from the in-test server");
CHECK(atomic_load(&results[2]) == 1, "httpGet returns the response body verbatim");
CHECK(atomic_load(&results[3]) == 1, "response headers are keyed by lowercased name");
CHECK(atomic_load(&results[4]) == 1, "a refused connection raises a catchable error");
CHECK(atomic_load(&results[5]) == 1, "an unsupported URL scheme raises a catchable error");
CHECK(atomic_load(&results[6]) == 1, "a hostile oversized chunk size is rejected cleanly, not hung");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

157
tests/testJson.c Normal file
View file

@ -0,0 +1,157 @@
// testJson.c -- exercises the JSON library: parse, stringify, round-trip, types, escapes,
// nesting, and error handling, driven from a Lua context.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogJson.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogJsonRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx,
"local o = jsonParse('{\"a\":1,\"b\":2,\"nested\":{\"c\":3}}')\n"
"report(1, o.a + o.b + o.nested.c)\n" // 6, nested object
"local arr = jsonParse('[10,20,30]')\n"
"report(2, arr[1] + arr[2] + arr[3])\n" // 60, array
"local o2 = jsonParse(jsonStringify({x=5, y=7}))\n"
"report(3, o2.x + o2.y)\n" // 12, round-trip a map
"local back = jsonParse(jsonStringify('a\\nb\"c'))\n"
"report(4, back == 'a\\nb\"c' and 1 or 0)\n" // 1, escape round-trip
"report(5, jsonParse('3.5') * 2)\n" // 7, real
"report(6, (jsonParse('true') and jsonParse('null') == nil) and 1 or 0)\n" // 1
"local ok = pcall(function() jsonParse('{not valid}') end)\n"
"report(7, ok and 0 or 1)\n" // 1, parse error is catchable
"local oks = pcall(function() jsonParse([[\"\\uDC00\"]]) end)\n"
"report(8, oks and 0 or 1)\n" // 1, a lone low surrogate is rejected
"report(9, math.type(jsonParse('99999999999999999999')) == 'float' and 1 or 0)\n" // 1, an int that overflows int64 becomes a real
"done()");
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 6, "parse a nested object");
CHECK(atomic_load(&results[2]) == 60, "parse an array");
CHECK(atomic_load(&results[3]) == 12, "round-trip a map through stringify + parse");
CHECK(atomic_load(&results[4]) == 1, "strings with escapes round-trip");
CHECK(atomic_load(&results[5]) == 7, "parse a real number");
CHECK(atomic_load(&results[6]) == 1, "parse true and null");
CHECK(atomic_load(&results[7]) == 1, "invalid JSON raises a catchable error");
CHECK(atomic_load(&results[8]) == 1, "an unpaired low surrogate is rejected");
CHECK(atomic_load(&results[9]) == 1, "an integer literal overflowing int64 parses as a real");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

170
tests/testKv.c Normal file
View file

@ -0,0 +1,170 @@
// testKv.c -- exercises the kv library: set/get scalars and tables, deep-copy independence,
// has/delete, key listing, and rejection of function values, driven from a Lua context.
// calogKvShutdown() runs BEFORE calogDestroy (like testExport).
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogKv.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogKvRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx,
"kvSet('n', 42)\n"
"report(1, kvGet('n'))\n" // 42, store + fetch a scalar
"kvSet('x', 1)\n"
"local had = kvHas('x')\n"
"kvDelete('x')\n"
"report(2, (had and not kvHas('x')) and 1 or 0)\n" // 1, present then gone
"kvSet('t', {a=10, b=20})\n"
"local t = kvGet('t')\n"
"report(3, t.a + t.b)\n" // 30, store + fetch a table
"t.a = 999\n"
"local t2 = kvGet('t')\n"
"report(4, t2.a)\n" // 10, get returns an independent copy
"report(5, kvGet('missing') == nil and 1 or 0)\n" // 1, missing key -> nil
"local ok = pcall(function() kvSet('fn', function() return 1 end) end)\n"
"report(6, ok and 0 or 1)\n" // 1, function value is rejected
"report(7, kvHas('fn') and 0 or 1)\n" // 1, nothing was stored under 'fn'
"local okn = pcall(function() kvSet('nested', {cb = function() return 1 end}) end)\n"
"report(10, (okn == false and kvHas('nested') == false) and 1 or 0)\n" // 1, a fn nested in a table is rejected too
"kvSet('k1', 1)\n"
"kvSet('k2', 2)\n"
"kvSet('k3', 3)\n"
"report(8, #kvKeys())\n" // 5, keys: n, t, k1, k2, k3
"local found = 0\n"
"for _, k in ipairs(kvKeys()) do if k == 'n' then found = 1 end end\n"
"report(9, found)\n" // 1, keys come back as strings
"done()");
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 42, "kvGet returns a stored scalar");
CHECK(atomic_load(&results[2]) == 1, "kvHas is false after kvDelete");
CHECK(atomic_load(&results[3]) == 30, "kvGet returns a stored table");
CHECK(atomic_load(&results[4]) == 10, "kvGet returns an independent deep copy");
CHECK(atomic_load(&results[5]) == 1, "kvGet on a missing key returns nil");
CHECK(atomic_load(&results[6]) == 1, "kvSet rejects a function value");
CHECK(atomic_load(&results[7]) == 1, "a rejected function value is not stored");
CHECK(atomic_load(&results[8]) == 5, "kvKeys lists every stored key");
CHECK(atomic_load(&results[9]) == 1, "kvKeys returns keys as strings");
CHECK(atomic_load(&results[10]) == 1, "kvSet rejects a function nested inside an aggregate");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogKvShutdown();
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

201
tests/testPubsub.c Normal file
View file

@ -0,0 +1,201 @@
// testPubsub.c -- exercises the pubsub library. Two Lua contexts subscribe to topic "t"; a
// third Lua context publishes. publish reaches both subscribers (count 2); after one context
// unsubscribes, publish reaches only the other (count 1); a published table payload is
// deep-copied and read back by the surviving subscriber; publishing an unheard topic returns
// 0; and publishing after calogPubsubShutdown is safe (the static registry is empty).
//
// The publisher is deliberately a SEPARATE context that is not itself subscribed to "t":
// publish runs inline on the publisher's thread and calogFnInvoke marshals each delivery to
// the subscriber's own thread, so a publisher that was subscribed would block waiting for
// itself (synchronous delivery is cyclic-deadlock-prone -- see calogPubsub.h).
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogPubsub.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *subA;
CalogContextT *subB;
CalogContextT *pub;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
if (calogPubsubRegister(calog) != calogOkE) {
printf("calogPubsubRegister failed\n");
return 1;
}
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
// Two independent Lua contexts each subscribe to "t". Each callback records the payload it
// received into its own result slot (unwrapping a table's .v field so report always gets a
// number). subA keeps its subscription id in a global so a later eval can unsubscribe it.
subA = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(subA,
"subAId = subscribe('t', function(m)\n"
" if type(m) == 'table' then report(1, m.v) else report(1, m) end\n"
"end)\n"
"done()");
subB = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(subB,
"subscribe('t', function(m)\n"
" if type(m) == 'table' then report(2, m.v) else report(2, m) end\n"
"end)\n"
"done()");
pumpUntilDone(2);
// A third context (NOT subscribed to "t") publishes a number: both subscribers run.
pub = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(pub, "report(3, publish('t', 7))\n done()");
pumpUntilDone(3);
CHECK(atomic_load(&results[3]) == 2, "publish reached both subscribers (count 2)");
CHECK(atomic_load(&results[1]) == 7, "subscriber A received the published number");
CHECK(atomic_load(&results[2]) == 7, "subscriber B received the published number");
// Unsubscribe A, then publish again: only B is invoked, and A's slot stays untouched.
atomic_store(&results[1], -1);
atomic_store(&results[2], -1);
calogContextEval(subA, "unsubscribe(subAId)\n done()");
pumpUntilDone(4);
calogContextEval(pub, "report(4, publish('t', 9))\n done()");
pumpUntilDone(5);
CHECK(atomic_load(&results[4]) == 1, "after unsubscribe only one subscriber is invoked (count 1)");
CHECK(atomic_load(&results[2]) == 9, "the surviving subscriber received the number");
CHECK(atomic_load(&results[1]) == -1, "the unsubscribed subscriber was not invoked");
// Publish a table payload: publish deep-copies it, so B reads m.v out of its own copy.
atomic_store(&results[2], -1);
calogContextEval(pub, "report(6, publish('t', {v = 42}))\n done()");
pumpUntilDone(6);
CHECK(atomic_load(&results[6]) == 1, "publishing a table reached the surviving subscriber (count 1)");
CHECK(atomic_load(&results[2]) == 42, "the subscriber received the deep-copied table payload");
// A topic nobody subscribed to delivers to no one.
calogContextEval(pub, "report(7, publish('none', 5))\n done()");
pumpUntilDone(7);
CHECK(atomic_load(&results[7]) == 0, "publishing to a topic with no subscribers returns 0");
CHECK(atomic_load(&errorCount) == 0, "no errors during pubsub");
// Release every subscribed callback while the contexts are still alive, then confirm a
// publish after shutdown is safe and simply finds no subscribers.
calogPubsubShutdown();
calogContextEval(pub, "report(11, publish('t', 1))\n done()");
pumpUntilDone(8);
CHECK(atomic_load(&results[11]) == 0, "publishing after shutdown is safe and finds no subscribers");
calogContextClose(subA);
calogContextClose(subB);
calogContextClose(pub);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

146
tests/testTime.c Normal file
View file

@ -0,0 +1,146 @@
// testTime.c -- exercises the time library: wall-clock and monotonic clock reads plus a
// millisecond sleep, driven from a Lua context.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogTime.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogTimeRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx,
"report(1, (timeNow() > 1600000000) and 1 or 0)\n" // wall clock is past 2020
"report(2, (timeMonotonic() >= 0) and 1 or 0)\n" // monotonic clock is non-negative
"local a = timeMonotonic()\n"
"local slept = timeSleep(20)\n"
"local b = timeMonotonic()\n"
"report(3, (b - a >= 0.015) and 1 or 0)\n" // ~20ms elapsed across the sleep
"report(4, (slept == nil) and 1 or 0)\n" // sleep returns nil
"report(5, (timeSleep(5.0) == nil) and 1 or 0)\n" // ms may be a real
"done()");
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 1, "timeNow is past 2020");
CHECK(atomic_load(&results[2]) == 1, "timeMonotonic is non-negative");
CHECK(atomic_load(&results[3]) == 1, "monotonic clock advances at least ~15ms across a 20ms sleep");
CHECK(atomic_load(&results[4]) == 1, "timeSleep returns nil");
CHECK(atomic_load(&results[5]) == 1, "timeSleep accepts a real millisecond count");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogContextClose(ctx);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}

190
tests/testTimer.c Normal file
View file

@ -0,0 +1,190 @@
// testTimer.c -- exercises the timer library from Lua: a one-shot fires once after a delay, a
// cancelled one-shot never fires, and a repeating timer fires several times before cancelling
// itself. Each callback runs on its owning context and reports back through host natives, which
// the main thread services by pumping.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogTimer.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 12
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpSettle(void);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpSettle(void) {
struct timespec ts = { 0, 500000 };
int i;
// Keep pumping for ~60ms so a broken cancel or a stray extra fire has time to show up.
for (i = 0; i < 120; i++) {
calogPump(calog);
nanosleep(&ts, NULL);
}
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctxAfter;
CalogContextT *ctxCancel;
CalogContextT *ctxEvery;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
if (calogTimerRegister(calog) != calogOkE) {
printf("calogTimerRegister failed\n");
return 1;
}
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
// A one-shot fires its callback once, ~10ms from now, on its owning context.
ctxAfter = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctxAfter,
"timerAfter(10, function() report(1, 42); done() end)");
// A one-shot cancelled immediately must NEVER fire (slot 3 stays -1). Slot 4 confirms the
// script itself ran to completion.
ctxCancel = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctxCancel,
"local h = timerAfter(20, function() report(3, 999) end)\n"
"timerCancel(h)\n"
"report(4, 1)\n"
"local okr = pcall(function() timerAfter(1/0, function() end) end)\n"
"report(5, okr and 0 or 1)\n" // 1, a non-finite delay is rejected, not UB
"done()");
// A repeating timer fires every ~5ms; after the third fire it cancels itself and reports the
// final count. Slot 2 must settle at exactly 3.
ctxEvery = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctxEvery,
"local count = 0\n"
"local id\n"
"id = timerEvery(5, function()\n"
" count = count + 1\n"
" report(2, count)\n"
" if count >= 3 then timerCancel(id); done() end\n"
"end)");
pumpUntilDone(3);
pumpSettle();
CHECK(atomic_load(&results[1]) == 42, "timerAfter fires its callback once after the delay");
CHECK(atomic_load(&results[4]) == 1, "a script that cancels a pending one-shot still runs to completion");
CHECK(atomic_load(&results[3]) == -1, "timerCancel of a one-shot before it fires prevents the callback");
CHECK(atomic_load(&results[2]) == 3, "timerEvery fires repeatedly and timerCancel stops it after 3 fires");
CHECK(atomic_load(&results[5]) == 1, "a non-finite / out-of-range delay is rejected");
CHECK(atomic_load(&doneCount) >= 3, "all three timer scripts completed");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
// Release pending callbacks + stop the thread while the contexts are still alive.
calogTimerShutdown();
calogContextClose(ctxAfter);
calogContextClose(ctxCancel);
calogContextClose(ctxEvery);
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}