From 144c3f1ca21425df90a7f752e18179d0249ae557 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 3 Jul 2026 02:13:23 -0500 Subject: [PATCH] More libs added. --- Makefile | 52 ++- libs/calogCrypto.c | 346 ++++++++++++++++ libs/calogCrypto.h | 26 ++ libs/calogFs.c | 352 ++++++++++++++++ libs/calogFs.h | 26 ++ libs/calogHttp.c | 984 +++++++++++++++++++++++++++++++++++++++++++++ libs/calogHttp.h | 26 ++ libs/calogJson.c | 762 +++++++++++++++++++++++++++++++++++ libs/calogJson.h | 21 + libs/calogKv.c | 286 +++++++++++++ libs/calogKv.h | 30 ++ libs/calogPubsub.c | 229 +++++++++++ libs/calogPubsub.h | 41 ++ libs/calogTime.c | 91 +++++ libs/calogTime.h | 20 + libs/calogTimer.c | 350 ++++++++++++++++ libs/calogTimer.h | 29 ++ tests/testCrypto.c | 165 ++++++++ tests/testFs.c | 178 ++++++++ tests/testHttp.c | 233 +++++++++++ tests/testJson.c | 157 ++++++++ tests/testKv.c | 170 ++++++++ tests/testPubsub.c | 201 +++++++++ tests/testTime.c | 146 +++++++ tests/testTimer.c | 190 +++++++++ 25 files changed, 5108 insertions(+), 3 deletions(-) create mode 100644 libs/calogCrypto.c create mode 100644 libs/calogCrypto.h create mode 100644 libs/calogFs.c create mode 100644 libs/calogFs.h create mode 100644 libs/calogHttp.c create mode 100644 libs/calogHttp.h create mode 100644 libs/calogJson.c create mode 100644 libs/calogJson.h create mode 100644 libs/calogKv.c create mode 100644 libs/calogKv.h create mode 100644 libs/calogPubsub.c create mode 100644 libs/calogPubsub.h create mode 100644 libs/calogTime.c create mode 100644 libs/calogTime.h create mode 100644 libs/calogTimer.c create mode 100644 libs/calogTimer.h create mode 100644 tests/testCrypto.c create mode 100644 tests/testFs.c create mode 100644 tests/testHttp.c create mode 100644 tests/testJson.c create mode 100644 tests/testKv.c create mode 100644 tests/testPubsub.c create mode 100644 tests/testTime.c create mode 100644 tests/testTimer.c diff --git a/Makefile b/Makefile index a8834f5..17af213 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/libs/calogCrypto.c b/libs/calogCrypto.c new file mode 100644 index 0000000..c01be08 --- /dev/null +++ b/libs/calogCrypto.c @@ -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 +#include +#include + +#include +#include +#include + +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); +} diff --git a/libs/calogCrypto.h b/libs/calogCrypto.h new file mode 100644 index 0000000..2a1d8fe --- /dev/null +++ b/libs/calogCrypto.h @@ -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 diff --git a/libs/calogFs.c b/libs/calogFs.c new file mode 100644 index 0000000..79b6552 --- /dev/null +++ b/libs/calogFs.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/libs/calogFs.h b/libs/calogFs.h new file mode 100644 index 0000000..8a43cda --- /dev/null +++ b/libs/calogFs.h @@ -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 diff --git a/libs/calogHttp.c b/libs/calogHttp.c new file mode 100644 index 0000000..692ebbc --- /dev/null +++ b/libs/calogHttp.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; +} diff --git a/libs/calogHttp.h b/libs/calogHttp.h new file mode 100644 index 0000000..ea25988 --- /dev/null +++ b/libs/calogHttp.h @@ -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 diff --git a/libs/calogJson.c b/libs/calogJson.c new file mode 100644 index 0000000..7658d89 --- /dev/null +++ b/libs/calogJson.c @@ -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 +#include +#include +#include +#include +#include + +#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; +} diff --git a/libs/calogJson.h b/libs/calogJson.h new file mode 100644 index 0000000..0810ae1 --- /dev/null +++ b/libs/calogJson.h @@ -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 diff --git a/libs/calogKv.c b/libs/calogKv.c new file mode 100644 index 0000000..3858e44 --- /dev/null +++ b/libs/calogKv.c @@ -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 +#include +#include +#include + +#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(©, &args[1]); + if (status != calogOkE) { + calogValueFree(©); + 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(©); + 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(©); + 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; +} diff --git a/libs/calogKv.h b/libs/calogKv.h new file mode 100644 index 0000000..e1213c7 --- /dev/null +++ b/libs/calogKv.h @@ -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 diff --git a/libs/calogPubsub.c b/libs/calogPubsub.c new file mode 100644 index 0000000..f0219a6 --- /dev/null +++ b/libs/calogPubsub.c @@ -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 +#include +#include +#include + +#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; +} diff --git a/libs/calogPubsub.h b/libs/calogPubsub.h new file mode 100644 index 0000000..526352c --- /dev/null +++ b/libs/calogPubsub.h @@ -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 diff --git a/libs/calogTime.c b/libs/calogTime.c new file mode 100644 index 0000000..ebca691 --- /dev/null +++ b/libs/calogTime.c @@ -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 +#include + +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; +} diff --git a/libs/calogTime.h b/libs/calogTime.h new file mode 100644 index 0000000..4fa8616 --- /dev/null +++ b/libs/calogTime.h @@ -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 diff --git a/libs/calogTimer.c b/libs/calogTimer.c new file mode 100644 index 0000000..293b691 --- /dev/null +++ b/libs/calogTimer.c @@ -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 +#include +#include +#include + +#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; +} diff --git a/libs/calogTimer.h b/libs/calogTimer.h new file mode 100644 index 0000000..c824ec9 --- /dev/null +++ b/libs/calogTimer.h @@ -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 diff --git a/tests/testCrypto.c b/tests/testCrypto.c new file mode 100644 index 0000000..3301567 --- /dev/null +++ b/tests/testCrypto.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/testFs.c b/tests/testFs.c new file mode 100644 index 0000000..2e6ba58 --- /dev/null +++ b/tests/testFs.c @@ -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 +#include +#include +#include + +#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; +} diff --git a/tests/testHttp.c b/tests/testHttp.c new file mode 100644 index 0000000..3eb562c --- /dev/null +++ b/tests/testHttp.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tests/testJson.c b/tests/testJson.c new file mode 100644 index 0000000..3ba4f3b --- /dev/null +++ b/tests/testJson.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/testKv.c b/tests/testKv.c new file mode 100644 index 0000000..817b34c --- /dev/null +++ b/tests/testKv.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/testPubsub.c b/tests/testPubsub.c new file mode 100644 index 0000000..431d3d0 --- /dev/null +++ b/tests/testPubsub.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/testTime.c b/tests/testTime.c new file mode 100644 index 0000000..7dfc470 --- /dev/null +++ b/tests/testTime.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/testTimer.c b/tests/testTimer.c new file mode 100644 index 0000000..9d6cec3 --- /dev/null +++ b/tests/testTimer.c @@ -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 +#include +#include + +#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; +}