More libs added.
This commit is contained in:
parent
82c040395d
commit
144c3f1ca2
25 changed files with 5108 additions and 3 deletions
52
Makefile
52
Makefile
|
|
@ -136,7 +136,7 @@ ENETOBJ = $(foreach n,$(ENETNAMES),obj/enet_$(n).o)
|
||||||
|
|
||||||
BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \
|
BINS = bin/testBroker bin/testLua bin/testMyBasic bin/testPolyglot bin/testActor \
|
||||||
bin/testEngineLua bin/testEngineMyBasic bin/testSquirrel bin/testEngineSquirrel bin/testJs bin/testEngineJs \
|
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)
|
all: $(BINS)
|
||||||
|
|
||||||
|
|
@ -148,10 +148,15 @@ $(STRICTOBJ): obj/%.o: %.c | obj
|
||||||
$(CC) $(COREFLAGS) $(INC) -c -o $@ $<
|
$(CC) $(COREFLAGS) $(INC) -c -o $@ $<
|
||||||
|
|
||||||
# strict C, threaded
|
# 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
|
$(THREADOBJ): obj/%.o: %.c | obj
|
||||||
$(CC) $(COREFLAGS) $(INC) -pthread -c -o $@ $<
|
$(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
|
# relaxed C + Lua headers
|
||||||
LUAADP = obj/luaAdapter.o obj/testLua.o
|
LUAADP = obj/luaAdapter.o obj/testLua.o
|
||||||
$(LUAADP): obj/%.o: %.c | obj
|
$(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
|
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
|
$(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:
|
obj bin lib:
|
||||||
mkdir -p $@
|
mkdir -p $@
|
||||||
|
|
||||||
test: all
|
test: all
|
||||||
./bin/testBroker && ./bin/testLua && ./bin/testMyBasic && ./bin/testPolyglot && \
|
./bin/testBroker && ./bin/testLua && ./bin/testMyBasic && ./bin/testPolyglot && \
|
||||||
./bin/testActor && ./bin/testEngineLua && ./bin/testEngineMyBasic && ./bin/testSquirrel && ./bin/testEngineSquirrel && \
|
./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
|
# 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
|
# with ASan). Recompiled from source under TSan; the vendored Lua objects are
|
||||||
|
|
@ -488,6 +517,23 @@ tsanwren: | bin obj
|
||||||
setarch -R ./bin/testEngineWrenTsan
|
setarch -R ./bin/testEngineWrenTsan
|
||||||
rm -f obj/wren.tsan.o
|
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:
|
clean:
|
||||||
rm -rf obj bin lib
|
rm -rf obj bin lib
|
||||||
|
|
||||||
|
|
|
||||||
346
libs/calogCrypto.c
Normal file
346
libs/calogCrypto.c
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
// calogCrypto.c -- calog cryptography library (see calogCrypto.h). Thin, binary-safe
|
||||||
|
// bindings over OpenSSL's one-shot primitives (EVP_Digest, HMAC, RAND_bytes,
|
||||||
|
// EVP_EncodeBlock/EVP_DecodeBlock): SHA-256/SHA-1 hashing, HMAC-SHA-256, random bytes,
|
||||||
|
// base64/hex codecs, and version-4 UUIDs. No shared state: every native is a pure function
|
||||||
|
// of its arguments.
|
||||||
|
|
||||||
|
#include "calogCrypto.h"
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <openssl/hmac.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
|
static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length);
|
||||||
|
static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage);
|
||||||
|
static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoHexNibble(unsigned char c);
|
||||||
|
static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogCryptoRegister(CalogT *calog) {
|
||||||
|
calogRegisterInline(calog, "hashSha256", cryptoHashSha256Native, NULL);
|
||||||
|
calogRegisterInline(calog, "hashSha1", cryptoHashSha1Native, NULL);
|
||||||
|
calogRegisterInline(calog, "hmacSha256", cryptoHmacSha256Native, NULL);
|
||||||
|
calogRegisterInline(calog, "randomBytes", cryptoRandomBytesNative, NULL);
|
||||||
|
calogRegisterInline(calog, "base64Encode", cryptoBase64EncodeNative, NULL);
|
||||||
|
calogRegisterInline(calog, "base64Decode", cryptoBase64DecodeNative, NULL);
|
||||||
|
calogRegisterInline(calog, "hexEncode", cryptoHexEncodeNative, NULL);
|
||||||
|
calogRegisterInline(calog, "hexDecode", cryptoHexDecodeNative, NULL);
|
||||||
|
calogRegisterInline(calog, "uuid", cryptoUuidNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
const unsigned char *in;
|
||||||
|
unsigned char *out;
|
||||||
|
int64_t length;
|
||||||
|
size_t outCap;
|
||||||
|
int32_t decoded;
|
||||||
|
int32_t pad;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "base64Decode expects (text)");
|
||||||
|
}
|
||||||
|
length = args[0].as.s.length;
|
||||||
|
in = (const unsigned char *)args[0].as.s.bytes;
|
||||||
|
// EVP_DecodeBlock ignores trailing whitespace before decoding; trim it here too so the
|
||||||
|
// decoded length and the '=' padding count are computed from the real base64 content
|
||||||
|
// (otherwise "YQ==\n" would decode with a spurious trailing NUL).
|
||||||
|
while (length > 0) {
|
||||||
|
unsigned char c;
|
||||||
|
c = in[length - 1];
|
||||||
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v') {
|
||||||
|
length--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
return calogValueString(result, "", 0);
|
||||||
|
}
|
||||||
|
if ((length % 4) != 0 || length > INT_MAX) {
|
||||||
|
return calogFail(result, calogErrArgE, "base64Decode: invalid base64 length");
|
||||||
|
}
|
||||||
|
outCap = ((size_t)length / 4) * 3;
|
||||||
|
out = (unsigned char *)malloc(outCap);
|
||||||
|
if (out == NULL) {
|
||||||
|
return calogFail(result, calogErrOomE, "base64Decode: out of memory");
|
||||||
|
}
|
||||||
|
// EVP_DecodeBlock always emits a multiple of three bytes and does NOT drop the bytes that
|
||||||
|
// correspond to '=' padding, so trim one output byte per trailing '=' ourselves.
|
||||||
|
decoded = EVP_DecodeBlock(out, in, (int)length);
|
||||||
|
if (decoded < 0) {
|
||||||
|
free(out);
|
||||||
|
return calogFail(result, calogErrArgE, "base64Decode: invalid base64 data");
|
||||||
|
}
|
||||||
|
pad = 0;
|
||||||
|
if (in[length - 1] == '=') {
|
||||||
|
pad++;
|
||||||
|
if (in[length - 2] == '=') {
|
||||||
|
pad++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = calogValueString(result, (const char *)out, (int64_t)(decoded - pad));
|
||||||
|
free(out);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "base64Decode: out of memory");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
unsigned char *out;
|
||||||
|
size_t inLen;
|
||||||
|
size_t outCap;
|
||||||
|
int32_t written;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "base64Encode expects (data)");
|
||||||
|
}
|
||||||
|
if (args[0].as.s.length > INT_MAX) {
|
||||||
|
return calogFail(result, calogErrRangeE, "base64Encode: input too large");
|
||||||
|
}
|
||||||
|
inLen = (size_t)args[0].as.s.length;
|
||||||
|
if (inLen == 0) {
|
||||||
|
return calogValueString(result, "", 0);
|
||||||
|
}
|
||||||
|
outCap = ((inLen + 2) / 3) * 4 + 1; // 4 base64 chars per 3 input bytes, plus the NUL EVP writes
|
||||||
|
out = (unsigned char *)malloc(outCap);
|
||||||
|
if (out == NULL) {
|
||||||
|
return calogFail(result, calogErrOomE, "base64Encode: out of memory");
|
||||||
|
}
|
||||||
|
written = EVP_EncodeBlock(out, (const unsigned char *)args[0].as.s.bytes, (int)inLen);
|
||||||
|
status = calogValueString(result, (const char *)out, (int64_t)written);
|
||||||
|
free(out);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "base64Encode: out of memory");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length) {
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
char *hex;
|
||||||
|
size_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return calogValueString(result, "", 0);
|
||||||
|
}
|
||||||
|
hex = (char *)malloc(length * 2);
|
||||||
|
if (hex == NULL) {
|
||||||
|
return calogErrOomE;
|
||||||
|
}
|
||||||
|
for (index = 0; index < length; index++) {
|
||||||
|
unsigned char byte;
|
||||||
|
byte = bytes[index];
|
||||||
|
hex[index * 2] = digits[byte >> 4];
|
||||||
|
hex[index * 2 + 1] = digits[byte & 0x0F];
|
||||||
|
}
|
||||||
|
status = calogValueString(result, hex, (int64_t)(length * 2));
|
||||||
|
free(hex);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage) {
|
||||||
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
||||||
|
unsigned int digestLen;
|
||||||
|
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, usage);
|
||||||
|
}
|
||||||
|
digestLen = 0;
|
||||||
|
if (EVP_Digest(args[0].as.s.bytes, (size_t)args[0].as.s.length, digest, &digestLen, md, NULL) != 1) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "digest computation failed");
|
||||||
|
}
|
||||||
|
return cryptoBytesToHex(result, digest, (size_t)digestLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
return cryptoDigestHex(args, argCount, result, EVP_sha1(), "hashSha1 expects (data)");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
return cryptoDigestHex(args, argCount, result, EVP_sha256(), "hashSha256 expects (data)");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
const unsigned char *in;
|
||||||
|
unsigned char *out;
|
||||||
|
int64_t length;
|
||||||
|
int64_t outLen;
|
||||||
|
int64_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "hexDecode expects (hexText)");
|
||||||
|
}
|
||||||
|
length = args[0].as.s.length;
|
||||||
|
if ((length % 2) != 0) {
|
||||||
|
return calogFail(result, calogErrArgE, "hexDecode: odd-length hex string");
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
return calogValueString(result, "", 0);
|
||||||
|
}
|
||||||
|
in = (const unsigned char *)args[0].as.s.bytes;
|
||||||
|
outLen = length / 2;
|
||||||
|
out = (unsigned char *)malloc((size_t)outLen);
|
||||||
|
if (out == NULL) {
|
||||||
|
return calogFail(result, calogErrOomE, "hexDecode: out of memory");
|
||||||
|
}
|
||||||
|
for (index = 0; index < outLen; index++) {
|
||||||
|
int32_t hi;
|
||||||
|
int32_t lo;
|
||||||
|
hi = cryptoHexNibble(in[index * 2]);
|
||||||
|
lo = cryptoHexNibble(in[index * 2 + 1]);
|
||||||
|
if (hi < 0 || lo < 0) {
|
||||||
|
free(out);
|
||||||
|
return calogFail(result, calogErrArgE, "hexDecode: invalid hex digit");
|
||||||
|
}
|
||||||
|
out[index] = (unsigned char)((hi << 4) | lo);
|
||||||
|
}
|
||||||
|
status = calogValueString(result, (const char *)out, outLen);
|
||||||
|
free(out);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "hexDecode: out of memory");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "hexEncode expects (data)");
|
||||||
|
}
|
||||||
|
return cryptoBytesToHex(result, (const unsigned char *)args[0].as.s.bytes, (size_t)args[0].as.s.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHexNibble(unsigned char c) {
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
return (int32_t)(c - '0');
|
||||||
|
}
|
||||||
|
if (c >= 'a' && c <= 'f') {
|
||||||
|
return (int32_t)(c - 'a' + 10);
|
||||||
|
}
|
||||||
|
if (c >= 'A' && c <= 'F') {
|
||||||
|
return (int32_t)(c - 'A' + 10);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
||||||
|
unsigned int digestLen;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "hmacSha256 expects (key, data)");
|
||||||
|
}
|
||||||
|
if (args[0].as.s.length > INT_MAX) {
|
||||||
|
return calogFail(result, calogErrRangeE, "hmacSha256: key too large");
|
||||||
|
}
|
||||||
|
digestLen = 0;
|
||||||
|
if (HMAC(EVP_sha256(), args[0].as.s.bytes, (int)args[0].as.s.length, (const unsigned char *)args[1].as.s.bytes, (size_t)args[1].as.s.length, digest, &digestLen) == NULL) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "hmacSha256 computation failed");
|
||||||
|
}
|
||||||
|
return cryptoBytesToHex(result, digest, (size_t)digestLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
unsigned char *buf;
|
||||||
|
int64_t count;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogIntE) {
|
||||||
|
return calogFail(result, calogErrArgE, "randomBytes expects (count)");
|
||||||
|
}
|
||||||
|
count = args[0].as.i;
|
||||||
|
if (count < 0 || count > INT_MAX) {
|
||||||
|
return calogFail(result, calogErrRangeE, "randomBytes: count out of range");
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return calogValueString(result, "", 0);
|
||||||
|
}
|
||||||
|
buf = (unsigned char *)malloc((size_t)count);
|
||||||
|
if (buf == NULL) {
|
||||||
|
return calogFail(result, calogErrOomE, "randomBytes: out of memory");
|
||||||
|
}
|
||||||
|
if (RAND_bytes(buf, (int)count) != 1) {
|
||||||
|
free(buf);
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "randomBytes: RAND_bytes failed");
|
||||||
|
}
|
||||||
|
status = calogValueString(result, (const char *)buf, count);
|
||||||
|
free(buf);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "randomBytes: out of memory");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
unsigned char bytes[16];
|
||||||
|
char text[37];
|
||||||
|
int32_t bi;
|
||||||
|
int32_t ti;
|
||||||
|
|
||||||
|
(void)args;
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 0) {
|
||||||
|
return calogFail(result, calogErrArgE, "uuid expects no arguments");
|
||||||
|
}
|
||||||
|
if (RAND_bytes(bytes, (int)sizeof(bytes)) != 1) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "uuid: RAND_bytes failed");
|
||||||
|
}
|
||||||
|
bytes[6] = (unsigned char)((bytes[6] & 0x0F) | 0x40); // version 4
|
||||||
|
bytes[8] = (unsigned char)((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant
|
||||||
|
ti = 0;
|
||||||
|
for (bi = 0; bi < 16; bi++) {
|
||||||
|
if (bi == 4 || bi == 6 || bi == 8 || bi == 10) {
|
||||||
|
text[ti++] = '-';
|
||||||
|
}
|
||||||
|
text[ti++] = digits[bytes[bi] >> 4];
|
||||||
|
text[ti++] = digits[bytes[bi] & 0x0F];
|
||||||
|
}
|
||||||
|
text[ti] = '\0';
|
||||||
|
return calogValueString(result, text, 36);
|
||||||
|
}
|
||||||
26
libs/calogCrypto.h
Normal file
26
libs/calogCrypto.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// calogCrypto.h -- calog cryptography library.
|
||||||
|
//
|
||||||
|
// Bridges binary-safe calog strings to OpenSSL's one-shot primitives, so any engine gets
|
||||||
|
// hashing, HMAC, randomness, and common encodings without an engine-specific library:
|
||||||
|
// hashSha256(data) -> lowercase hex digest (64 chars)
|
||||||
|
// hashSha1(data) -> lowercase hex digest (40 chars)
|
||||||
|
// hmacSha256(key, data) -> lowercase hex HMAC-SHA-256 (64 chars)
|
||||||
|
// randomBytes(count) -> binary string of count cryptographically-random bytes
|
||||||
|
// base64Encode(data) -> base64 text
|
||||||
|
// base64Decode(text) -> decoded binary string
|
||||||
|
// hexEncode(data) -> lowercase hex text
|
||||||
|
// hexDecode(hexText) -> decoded binary string
|
||||||
|
// uuid() -> random RFC 4122 version-4 UUID string
|
||||||
|
// All natives are binary-safe and INLINE (pure computation over OpenSSL one-shots, no shared
|
||||||
|
// state), so there is nothing to shut down.
|
||||||
|
|
||||||
|
#ifndef CALOG_CRYPTO_H
|
||||||
|
#define CALOG_CRYPTO_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the crypto natives on a runtime. Stateless: safe to call on any number of
|
||||||
|
// runtimes, and there is no matching shutdown.
|
||||||
|
int32_t calogCryptoRegister(CalogT *calog);
|
||||||
|
|
||||||
|
#endif
|
||||||
352
libs/calogFs.c
Normal file
352
libs/calogFs.c
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
// calogFs.c -- calog filesystem library (see calogFs.h). Thin, binary-safe wrappers over the
|
||||||
|
// POSIX file API (open/read/write, stat, opendir/readdir, mkdir, unlink), bridging paths and
|
||||||
|
// file bytes to calog's value/aggregate model. No shared state: every native is a pure function
|
||||||
|
// of its arguments over the OS filesystem, so the natives are INLINE and there is nothing to
|
||||||
|
// shut down. A system-call failure becomes a calogFail carrying strerror(errno).
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogFs.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsFail(CalogValueT *result, int32_t err);
|
||||||
|
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value);
|
||||||
|
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result);
|
||||||
|
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogFsRegister(CalogT *calog) {
|
||||||
|
calogRegisterInline(calog, "fsRead", fsReadNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsWrite", fsWriteNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsAppend", fsAppendNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsExists", fsExistsNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsRemove", fsRemoveNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsMkdir", fsMkdirNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsList", fsListNative, NULL);
|
||||||
|
calogRegisterInline(calog, "fsStat", fsStatNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsAppend expects (path, data)");
|
||||||
|
}
|
||||||
|
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, true, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsExists expects (path)");
|
||||||
|
}
|
||||||
|
calogValueBool(result, access(args[0].as.s.bytes, F_OK) == 0);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Map a failed syscall's errno to a calog status (ENOENT and the general case both read as
|
||||||
|
// "not found", ENOMEM as OOM) and carry the human-readable strerror text as the message.
|
||||||
|
static int32_t fsFail(CalogValueT *result, int32_t err) {
|
||||||
|
if (err == ENOMEM) {
|
||||||
|
return calogFail(result, calogErrOomE, strerror(err));
|
||||||
|
}
|
||||||
|
return calogFail(result, calogErrNotFoundE, strerror(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct dirent *entry;
|
||||||
|
CalogValueT name;
|
||||||
|
CalogAggT *agg;
|
||||||
|
DIR *dir;
|
||||||
|
int32_t status;
|
||||||
|
int saved;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsList expects (path)");
|
||||||
|
}
|
||||||
|
dir = opendir(args[0].as.s.bytes);
|
||||||
|
if (dir == NULL) {
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
status = calogAggCreate(&agg, calogListE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
closedir(dir);
|
||||||
|
return calogFail(result, status, "fsList: out of memory");
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
errno = 0;
|
||||||
|
entry = readdir(dir);
|
||||||
|
if (entry == NULL) {
|
||||||
|
if (errno != 0) {
|
||||||
|
saved = errno;
|
||||||
|
calogAggFree(agg);
|
||||||
|
closedir(dir);
|
||||||
|
return fsFail(result, saved);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
status = calogValueString(&name, entry->d_name, (int64_t)strlen(entry->d_name));
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
closedir(dir);
|
||||||
|
return calogFail(result, status, "fsList: out of memory");
|
||||||
|
}
|
||||||
|
status = calogAggPush(agg, &name);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&name);
|
||||||
|
calogAggFree(agg);
|
||||||
|
closedir(dir);
|
||||||
|
return calogFail(result, status, "fsList: out of memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
calogValueAgg(result, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add keyName -> value to a map, taking ownership of value: on any failure both the freshly
|
||||||
|
// built key and the caller's value are freed, so the caller never double-frees.
|
||||||
|
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value) {
|
||||||
|
CalogValueT key;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
status = calogValueString(&key, keyName, (int64_t)strlen(keyName));
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(value);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = calogAggSet(agg, &key, value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&key);
|
||||||
|
calogValueFree(value);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct stat st;
|
||||||
|
const char *path;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsMkdir expects (path)");
|
||||||
|
}
|
||||||
|
path = args[0].as.s.bytes;
|
||||||
|
if (mkdir(path, 0777) != 0) {
|
||||||
|
// An already-existing directory is success; anything else (including a non-directory
|
||||||
|
// squatting on the name) is the failure the caller sees.
|
||||||
|
if (errno == EEXIST && stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Shared body of fsWrite (truncate) and fsAppend: create the file if needed and write exactly
|
||||||
|
// length bytes, honoring embedded NULs. On success result is nil.
|
||||||
|
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result) {
|
||||||
|
size_t offset;
|
||||||
|
size_t total;
|
||||||
|
int fd;
|
||||||
|
int flags;
|
||||||
|
int saved;
|
||||||
|
|
||||||
|
flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
|
||||||
|
fd = open(path, flags, 0666);
|
||||||
|
if (fd < 0) {
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
total = (length > 0) ? (size_t)length : (size_t)0;
|
||||||
|
offset = 0;
|
||||||
|
while (offset < total) {
|
||||||
|
ssize_t put;
|
||||||
|
put = write(fd, bytes + offset, total - offset);
|
||||||
|
if (put < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
saved = errno;
|
||||||
|
close(fd);
|
||||||
|
return fsFail(result, saved);
|
||||||
|
}
|
||||||
|
offset += (size_t)put;
|
||||||
|
}
|
||||||
|
if (close(fd) != 0) {
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct stat st;
|
||||||
|
const char *path;
|
||||||
|
char *data;
|
||||||
|
ssize_t got;
|
||||||
|
size_t offset;
|
||||||
|
size_t total;
|
||||||
|
int fd;
|
||||||
|
int saved;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsRead expects (path)");
|
||||||
|
}
|
||||||
|
path = args[0].as.s.bytes;
|
||||||
|
fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
if (fstat(fd, &st) != 0) {
|
||||||
|
saved = errno;
|
||||||
|
close(fd);
|
||||||
|
return fsFail(result, saved);
|
||||||
|
}
|
||||||
|
total = (st.st_size > 0) ? (size_t)st.st_size : (size_t)0;
|
||||||
|
data = (char *)malloc(total + 1);
|
||||||
|
if (data == NULL) {
|
||||||
|
close(fd);
|
||||||
|
return calogFail(result, calogErrOomE, "fsRead: out of memory");
|
||||||
|
}
|
||||||
|
offset = 0;
|
||||||
|
while (offset < total) {
|
||||||
|
got = read(fd, data + offset, total - offset);
|
||||||
|
if (got < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
saved = errno;
|
||||||
|
free(data);
|
||||||
|
close(fd);
|
||||||
|
return fsFail(result, saved);
|
||||||
|
}
|
||||||
|
if (got == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += (size_t)got;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
status = calogValueString(result, data, (int64_t)offset);
|
||||||
|
free(data);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsRemove expects (path)");
|
||||||
|
}
|
||||||
|
// unlink refuses a directory (EISDIR on Linux, EPERM elsewhere); remove an empty
|
||||||
|
// directory with rmdir instead, so fsRemove deletes either a file or an empty directory.
|
||||||
|
if (unlink(args[0].as.s.bytes) == 0) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
if ((errno == EISDIR || errno == EPERM) && rmdir(args[0].as.s.bytes) == 0) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct stat st;
|
||||||
|
CalogValueT value;
|
||||||
|
CalogAggT *agg;
|
||||||
|
const char *path;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsStat expects (path)");
|
||||||
|
}
|
||||||
|
path = args[0].as.s.bytes;
|
||||||
|
if (stat(path, &st) != 0) {
|
||||||
|
// A missing path is not an error: report nil so callers can probe with fsStat.
|
||||||
|
if (errno == ENOENT) {
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
return fsFail(result, errno);
|
||||||
|
}
|
||||||
|
status = calogAggCreate(&agg, calogMapE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "fsStat: out of memory");
|
||||||
|
}
|
||||||
|
calogValueInt(&value, (int64_t)st.st_size);
|
||||||
|
status = fsMapSet(agg, "size", &value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogFail(result, status, "fsStat: out of memory");
|
||||||
|
}
|
||||||
|
calogValueBool(&value, S_ISDIR(st.st_mode) != 0);
|
||||||
|
status = fsMapSet(agg, "isDir", &value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogFail(result, status, "fsStat: out of memory");
|
||||||
|
}
|
||||||
|
calogValueBool(&value, S_ISREG(st.st_mode) != 0);
|
||||||
|
status = fsMapSet(agg, "isFile", &value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogFail(result, status, "fsStat: out of memory");
|
||||||
|
}
|
||||||
|
calogValueInt(&value, (int64_t)st.st_mtime);
|
||||||
|
status = fsMapSet(agg, "mtime", &value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogFail(result, status, "fsStat: out of memory");
|
||||||
|
}
|
||||||
|
calogValueAgg(result, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "fsWrite expects (path, data)");
|
||||||
|
}
|
||||||
|
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, false, result);
|
||||||
|
}
|
||||||
26
libs/calogFs.h
Normal file
26
libs/calogFs.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// calogFs.h -- calog filesystem library.
|
||||||
|
//
|
||||||
|
// Bridges the POSIX filesystem into calog's value model so any engine can read, write, and
|
||||||
|
// inspect files without an engine-specific I/O library:
|
||||||
|
// fsRead(path) -> string (the whole file, binary-safe)
|
||||||
|
// fsWrite(path, data) -> nil (create/truncate, honoring data's byte length)
|
||||||
|
// fsAppend(path, data) -> nil (create if absent, append at the end)
|
||||||
|
// fsExists(path) -> bool
|
||||||
|
// fsRemove(path) -> nil (unlink a file)
|
||||||
|
// fsMkdir(path) -> nil (single level, mode 0777 & umask; an existing dir is OK)
|
||||||
|
// fsList(path) -> list (entry name strings, excluding "." and "..")
|
||||||
|
// fsStat(path) -> map {size, isDir, isFile, mtime} or nil if the path is absent
|
||||||
|
// The natives are INLINE (each is a pure function of its arguments over the OS filesystem, no
|
||||||
|
// shared state), so there is nothing to shut down. A failed operation surfaces as a catchable
|
||||||
|
// script error carrying strerror(errno).
|
||||||
|
|
||||||
|
#ifndef CALOG_FS_H
|
||||||
|
#define CALOG_FS_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the filesystem natives on a runtime. Stateless: safe to call on any number of
|
||||||
|
// runtimes, and there is no matching shutdown.
|
||||||
|
int32_t calogFsRegister(CalogT *calog);
|
||||||
|
|
||||||
|
#endif
|
||||||
984
libs/calogHttp.c
Normal file
984
libs/calogHttp.c
Normal file
|
|
@ -0,0 +1,984 @@
|
||||||
|
// calogHttp.c -- calog HTTP client library (see calogHttp.h). A self-contained HTTP/1.1
|
||||||
|
// client: parse the URL, connect (getaddrinfo + socket + connect), optionally wrap the socket
|
||||||
|
// in OpenSSL TLS for https, send the request with "Connection: close", read the entire
|
||||||
|
// response to EOF, and decode it (status line, lowercased headers into a map, and a body that
|
||||||
|
// honors Transfer-Encoding: chunked / Content-Length / read-to-close). No shared state: every
|
||||||
|
// native is a self-contained connection, so the natives are INLINE and there is nothing to
|
||||||
|
// shut down. TLS does NO certificate verification in v1 (documented in the header).
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include "calogHttp.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <openssl/ssl.h>
|
||||||
|
|
||||||
|
#define HTTP_BUF_INITIAL 256
|
||||||
|
#define HTTP_READ_CHUNK 16384
|
||||||
|
#define HTTP_MAX_RESPONSE (64 * 1024 * 1024)
|
||||||
|
#define HTTP_HOST_MAX 256
|
||||||
|
#define HTTP_PORT_MAX 16
|
||||||
|
#define HTTP_PATH_MAX 4096
|
||||||
|
|
||||||
|
// A growable output byte buffer for the request and for decoded bodies.
|
||||||
|
typedef struct HttpBufT {
|
||||||
|
char *bytes;
|
||||||
|
size_t length;
|
||||||
|
size_t cap;
|
||||||
|
} HttpBufT;
|
||||||
|
|
||||||
|
// A parsed absolute URL. host/port/path are NUL-terminated so they double as C strings.
|
||||||
|
typedef struct HttpUrlT {
|
||||||
|
bool https;
|
||||||
|
char host[HTTP_HOST_MAX];
|
||||||
|
char port[HTTP_PORT_MAX];
|
||||||
|
char path[HTTP_PATH_MAX];
|
||||||
|
} HttpUrlT;
|
||||||
|
|
||||||
|
// One connection: a plain socket, or a TLS session layered over it when ssl != NULL.
|
||||||
|
typedef struct HttpConnT {
|
||||||
|
int fd;
|
||||||
|
SSL *ssl;
|
||||||
|
SSL_CTX *ctx;
|
||||||
|
} HttpConnT;
|
||||||
|
|
||||||
|
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers);
|
||||||
|
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra);
|
||||||
|
static void httpBufFree(HttpBufT *buf);
|
||||||
|
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length);
|
||||||
|
static int32_t httpBufPutStr(HttpBufT *buf, const char *s);
|
||||||
|
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result);
|
||||||
|
static void httpConnClose(HttpConnT *conn);
|
||||||
|
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
|
||||||
|
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length);
|
||||||
|
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length);
|
||||||
|
static bool httpContainsChunked(const char *bytes, size_t length);
|
||||||
|
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length);
|
||||||
|
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out);
|
||||||
|
static bool httpDefaultPort(const HttpUrlT *url);
|
||||||
|
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t httpHexVal(char c);
|
||||||
|
static char httpLower(char c);
|
||||||
|
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner);
|
||||||
|
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value);
|
||||||
|
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length);
|
||||||
|
static int64_t httpParseInt(const char *bytes, size_t length);
|
||||||
|
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut);
|
||||||
|
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result);
|
||||||
|
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, CalogValueT *result);
|
||||||
|
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw);
|
||||||
|
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogHttpRegister(CalogT *calog) {
|
||||||
|
calogRegisterInline(calog, "httpGet", httpGetNative, NULL);
|
||||||
|
calogRegisterInline(calog, "httpRequest", httpRequestNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpAppendHeaders(HttpBufT *buf, const CalogAggT *headers) {
|
||||||
|
int64_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
// Only string:string pairs become request headers; anything else is skipped. The bytes are
|
||||||
|
// binary-safe (length-carried), so a header value may hold arbitrary octets.
|
||||||
|
for (index = 0; index < headers->pairCount; index++) {
|
||||||
|
const CalogValueT *key;
|
||||||
|
const CalogValueT *val;
|
||||||
|
key = &headers->pairs[index].key;
|
||||||
|
val = &headers->pairs[index].value;
|
||||||
|
if (key->type != calogStringE || val->type != calogStringE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
status = httpBufPutBytes(buf, key->as.s.bytes, (size_t)key->as.s.length);
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(buf, ": ");
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutBytes(buf, val->as.s.bytes, (size_t)val->as.s.length);
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(buf, "\r\n");
|
||||||
|
}
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpBufEnsure(HttpBufT *buf, size_t extra) {
|
||||||
|
size_t need;
|
||||||
|
size_t cap;
|
||||||
|
char *grown;
|
||||||
|
|
||||||
|
need = buf->length + extra;
|
||||||
|
if (need <= buf->cap) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
cap = (buf->cap == 0) ? HTTP_BUF_INITIAL : buf->cap;
|
||||||
|
while (cap < need) {
|
||||||
|
cap *= CALOG_GROWTH_FACTOR;
|
||||||
|
}
|
||||||
|
grown = (char *)realloc(buf->bytes, cap);
|
||||||
|
if (grown == NULL) {
|
||||||
|
return calogErrOomE;
|
||||||
|
}
|
||||||
|
buf->bytes = grown;
|
||||||
|
buf->cap = cap;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void httpBufFree(HttpBufT *buf) {
|
||||||
|
free(buf->bytes);
|
||||||
|
buf->bytes = NULL;
|
||||||
|
buf->length = 0;
|
||||||
|
buf->cap = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpBufPutBytes(HttpBufT *buf, const char *bytes, size_t length) {
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
status = httpBufEnsure(buf, length);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
memcpy(buf->bytes + buf->length, bytes, length);
|
||||||
|
buf->length += length;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpBufPutStr(HttpBufT *buf, const char *s) {
|
||||||
|
return httpBufPutBytes(buf, s, strlen(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpBuildResult(const char *raw, size_t rawLen, CalogValueT *result) {
|
||||||
|
const char *sep;
|
||||||
|
const char *headerBlock;
|
||||||
|
const char *bodyRaw;
|
||||||
|
const char *statusEnd;
|
||||||
|
const char *cursor;
|
||||||
|
const char *headerEnd;
|
||||||
|
const char *finalBody;
|
||||||
|
HttpBufT dechunked;
|
||||||
|
CalogAggT *map;
|
||||||
|
CalogAggT *headersMap;
|
||||||
|
size_t headerLen;
|
||||||
|
size_t bodyRawLen;
|
||||||
|
size_t statusLen;
|
||||||
|
size_t finalLen;
|
||||||
|
int64_t contentLength;
|
||||||
|
int32_t code;
|
||||||
|
int32_t status;
|
||||||
|
bool haveContentLength;
|
||||||
|
bool chunked;
|
||||||
|
|
||||||
|
calogValueNil(result);
|
||||||
|
dechunked.bytes = NULL;
|
||||||
|
dechunked.length = 0;
|
||||||
|
dechunked.cap = 0;
|
||||||
|
contentLength = 0;
|
||||||
|
haveContentLength = false;
|
||||||
|
chunked = false;
|
||||||
|
|
||||||
|
// Split the head from the body at the first blank line.
|
||||||
|
sep = (const char *)memmem(raw, rawLen, "\r\n\r\n", 4);
|
||||||
|
if (sep == NULL) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: malformed response (no header terminator)");
|
||||||
|
}
|
||||||
|
headerBlock = raw;
|
||||||
|
headerLen = (size_t)(sep - raw);
|
||||||
|
bodyRaw = sep + 4;
|
||||||
|
bodyRawLen = rawLen - headerLen - 4;
|
||||||
|
|
||||||
|
// The status line is the first CRLF-terminated line of the head.
|
||||||
|
statusEnd = (const char *)memmem(headerBlock, headerLen, "\r\n", 2);
|
||||||
|
if (statusEnd == NULL) {
|
||||||
|
statusLen = headerLen;
|
||||||
|
cursor = headerBlock + headerLen;
|
||||||
|
} else {
|
||||||
|
statusLen = (size_t)(statusEnd - headerBlock);
|
||||||
|
cursor = statusEnd + 2;
|
||||||
|
}
|
||||||
|
headerEnd = headerBlock + headerLen;
|
||||||
|
status = httpParseStatus(headerBlock, statusLen, &code);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: malformed status line");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header fields -> map keyed by lowercased name; capture the body-framing headers.
|
||||||
|
status = calogAggCreate(&headersMap, calogMapE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
while (cursor < headerEnd) {
|
||||||
|
const char *lineEnd;
|
||||||
|
const char *colon;
|
||||||
|
size_t lineLen;
|
||||||
|
lineEnd = (const char *)memmem(cursor, (size_t)(headerEnd - cursor), "\r\n", 2);
|
||||||
|
if (lineEnd == NULL) {
|
||||||
|
lineEnd = headerEnd;
|
||||||
|
}
|
||||||
|
lineLen = (size_t)(lineEnd - cursor);
|
||||||
|
colon = (lineLen > 0) ? (const char *)memchr(cursor, ':', lineLen) : NULL;
|
||||||
|
if (colon != NULL) {
|
||||||
|
const char *nameStart;
|
||||||
|
const char *valStart;
|
||||||
|
char *lower;
|
||||||
|
size_t nameLen;
|
||||||
|
size_t valLen;
|
||||||
|
size_t i;
|
||||||
|
nameStart = cursor;
|
||||||
|
nameLen = (size_t)(colon - cursor);
|
||||||
|
valStart = colon + 1;
|
||||||
|
valLen = (size_t)(lineEnd - valStart);
|
||||||
|
while (valLen > 0 && (*valStart == ' ' || *valStart == '\t')) {
|
||||||
|
valStart++;
|
||||||
|
valLen--;
|
||||||
|
}
|
||||||
|
while (valLen > 0 && (valStart[valLen - 1] == ' ' || valStart[valLen - 1] == '\t')) {
|
||||||
|
valLen--;
|
||||||
|
}
|
||||||
|
lower = (char *)malloc(nameLen + 1);
|
||||||
|
if (lower == NULL) {
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, calogErrOomE, "http: out of memory");
|
||||||
|
}
|
||||||
|
for (i = 0; i < nameLen; i++) {
|
||||||
|
lower[i] = httpLower(nameStart[i]);
|
||||||
|
}
|
||||||
|
lower[nameLen] = '\0';
|
||||||
|
status = httpMapSetStr(headersMap, lower, valStart, (int64_t)valLen);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
free(lower);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
if (strcmp(lower, "content-length") == 0) {
|
||||||
|
contentLength = httpParseInt(valStart, valLen);
|
||||||
|
if (contentLength >= 0) {
|
||||||
|
haveContentLength = true;
|
||||||
|
}
|
||||||
|
} else if (strcmp(lower, "transfer-encoding") == 0) {
|
||||||
|
if (httpContainsChunked(valStart, valLen)) {
|
||||||
|
chunked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(lower);
|
||||||
|
}
|
||||||
|
if (lineEnd >= headerEnd) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = lineEnd + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the body. Chunked takes precedence over Content-Length; absent both, read-to-close
|
||||||
|
// means the raw remainder is the body.
|
||||||
|
if (chunked) {
|
||||||
|
status = httpDechunk(bodyRaw, bodyRawLen, &dechunked);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: malformed chunked body");
|
||||||
|
}
|
||||||
|
finalBody = dechunked.bytes;
|
||||||
|
finalLen = dechunked.length;
|
||||||
|
} else if (haveContentLength) {
|
||||||
|
finalBody = bodyRaw;
|
||||||
|
finalLen = ((size_t)contentLength < bodyRawLen) ? (size_t)contentLength : bodyRawLen;
|
||||||
|
} else {
|
||||||
|
finalBody = bodyRaw;
|
||||||
|
finalLen = bodyRawLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the outer { status, body, headers } map.
|
||||||
|
status = calogAggCreate(&map, calogMapE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
status = httpMapSetInt(map, "status", (int64_t)code);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogAggFree(map);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
status = httpMapSetStr(map, "body", (finalBody != NULL) ? finalBody : "", (int64_t)finalLen);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogAggFree(map);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
status = httpMapSetAgg(map, "headers", headersMap);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogAggFree(map);
|
||||||
|
calogAggFree(headersMap);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
// headersMap is now owned by map; only our transient decode buffer remains to free.
|
||||||
|
httpBufFree(&dechunked);
|
||||||
|
calogValueAgg(result, map);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void httpConnClose(HttpConnT *conn) {
|
||||||
|
if (conn->ssl != NULL) {
|
||||||
|
SSL_shutdown(conn->ssl);
|
||||||
|
SSL_free(conn->ssl);
|
||||||
|
conn->ssl = NULL;
|
||||||
|
}
|
||||||
|
if (conn->ctx != NULL) {
|
||||||
|
SSL_CTX_free(conn->ctx);
|
||||||
|
conn->ctx = NULL;
|
||||||
|
}
|
||||||
|
// SSL_set_fd installs a BIO_NOCLOSE socket BIO, so SSL_free never closes the fd -- we own it.
|
||||||
|
if (conn->fd >= 0) {
|
||||||
|
close(conn->fd);
|
||||||
|
conn->fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpConnOpen(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
|
||||||
|
struct addrinfo hints;
|
||||||
|
struct addrinfo *res;
|
||||||
|
struct addrinfo *rp;
|
||||||
|
int fd;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
conn->fd = -1;
|
||||||
|
conn->ssl = NULL;
|
||||||
|
conn->ctx = NULL;
|
||||||
|
memset(&hints, 0, sizeof(hints));
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
rc = getaddrinfo(url->host, url->port, &hints, &res);
|
||||||
|
if (rc != 0) {
|
||||||
|
return calogFail(result, calogErrArgE, gai_strerror(rc));
|
||||||
|
}
|
||||||
|
fd = -1;
|
||||||
|
for (rp = res; rp != NULL; rp = rp->ai_next) {
|
||||||
|
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||||
|
if (fd < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
fd = -1;
|
||||||
|
}
|
||||||
|
freeaddrinfo(res);
|
||||||
|
if (fd < 0) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: could not connect to host");
|
||||||
|
}
|
||||||
|
conn->fd = fd;
|
||||||
|
if (url->https) {
|
||||||
|
int32_t status;
|
||||||
|
status = httpTlsHandshake(conn, url, result);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpConnClose(conn);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static ssize_t httpConnRead(HttpConnT *conn, char *buf, size_t length) {
|
||||||
|
if (conn->ssl != NULL) {
|
||||||
|
int chunk;
|
||||||
|
int r;
|
||||||
|
chunk = (length > (size_t)INT_MAX) ? INT_MAX : (int)length;
|
||||||
|
r = SSL_read(conn->ssl, buf, chunk);
|
||||||
|
// A non-positive return (clean shutdown or error) ends the read-to-EOF loop.
|
||||||
|
if (r <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (ssize_t)r;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
ssize_t r;
|
||||||
|
r = recv(conn->fd, buf, length, 0);
|
||||||
|
if (r < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpConnWriteAll(HttpConnT *conn, const char *bytes, size_t length) {
|
||||||
|
size_t sent;
|
||||||
|
|
||||||
|
sent = 0;
|
||||||
|
while (sent < length) {
|
||||||
|
if (conn->ssl != NULL) {
|
||||||
|
int chunk;
|
||||||
|
int w;
|
||||||
|
chunk = (length - sent > (size_t)INT_MAX) ? INT_MAX : (int)(length - sent);
|
||||||
|
w = SSL_write(conn->ssl, bytes + sent, chunk);
|
||||||
|
if (w <= 0) {
|
||||||
|
return calogErrUnsupportedE;
|
||||||
|
}
|
||||||
|
sent += (size_t)w;
|
||||||
|
} else {
|
||||||
|
ssize_t w;
|
||||||
|
w = send(conn->fd, bytes + sent, length - sent, MSG_NOSIGNAL);
|
||||||
|
if (w < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
sent += (size_t)w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool httpContainsChunked(const char *bytes, size_t length) {
|
||||||
|
static const char needle[] = "chunked";
|
||||||
|
size_t nlen;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
nlen = sizeof(needle) - 1;
|
||||||
|
if (length < nlen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (i = 0; i + nlen <= length; i++) {
|
||||||
|
size_t j;
|
||||||
|
bool match;
|
||||||
|
match = true;
|
||||||
|
for (j = 0; j < nlen; j++) {
|
||||||
|
if (httpLower(bytes[i + j]) != needle[j]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool httpCopyField(char *dst, size_t cap, const char *src, size_t length) {
|
||||||
|
if (length + 1 > cap) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(dst, src, length);
|
||||||
|
dst[length] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpDechunk(const char *bytes, size_t length, HttpBufT *out) {
|
||||||
|
size_t pos;
|
||||||
|
|
||||||
|
pos = 0;
|
||||||
|
while (pos < length) {
|
||||||
|
size_t size;
|
||||||
|
bool any;
|
||||||
|
int32_t status;
|
||||||
|
size = 0;
|
||||||
|
any = false;
|
||||||
|
while (pos < length) {
|
||||||
|
int32_t digit;
|
||||||
|
digit = httpHexVal(bytes[pos]);
|
||||||
|
if (digit < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Reject a chunk size that would overflow size_t (a hostile server can send a huge
|
||||||
|
// hex length to wrap the bounds check below).
|
||||||
|
if (size > (SIZE_MAX - (size_t)digit) / 16) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
size = size * 16 + (size_t)digit;
|
||||||
|
any = true;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
// Skip any chunk extension (";name=value") through the CRLF that ends the size line.
|
||||||
|
while (pos < length && bytes[pos] != '\n') {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (pos >= length) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
if (size == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (size > length - pos) { // pos <= length here, so no overflow
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
status = httpBufPutBytes(out, bytes + pos, size);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
pos += size;
|
||||||
|
if (pos < length && bytes[pos] == '\r') {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (pos < length && bytes[pos] == '\n') {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool httpDefaultPort(const HttpUrlT *url) {
|
||||||
|
if (url->https) {
|
||||||
|
return strcmp(url->port, "443") == 0;
|
||||||
|
}
|
||||||
|
return strcmp(url->port, "80") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpGetNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "httpGet expects (url)");
|
||||||
|
}
|
||||||
|
return httpPerform("GET", args[0].as.s.bytes, args[0].as.s.length, NULL, NULL, 0, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpHexVal(char c) {
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
return (int32_t)(c - '0');
|
||||||
|
}
|
||||||
|
if (c >= 'a' && c <= 'f') {
|
||||||
|
return (int32_t)(c - 'a' + 10);
|
||||||
|
}
|
||||||
|
if (c >= 'A' && c <= 'F') {
|
||||||
|
return (int32_t)(c - 'A' + 10);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static char httpLower(char c) {
|
||||||
|
if (c >= 'A' && c <= 'Z') {
|
||||||
|
return (char)(c - 'A' + 'a');
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpMapSetAgg(CalogAggT *map, const char *key, CalogAggT *inner) {
|
||||||
|
CalogValueT keyValue;
|
||||||
|
CalogValueT aggValue;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
calogValueAgg(&aggValue, inner);
|
||||||
|
status = calogAggSet(map, &keyValue, &aggValue);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
// calogAggSet did not take ownership; free the key but leave `inner` (which aggValue
|
||||||
|
// only wraps) for the caller to free.
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpMapSetInt(CalogAggT *map, const char *key, int64_t value) {
|
||||||
|
CalogValueT keyValue;
|
||||||
|
CalogValueT intValue;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
calogValueInt(&intValue, value);
|
||||||
|
status = calogAggSet(map, &keyValue, &intValue);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpMapSetStr(CalogAggT *map, const char *key, const char *bytes, int64_t length) {
|
||||||
|
CalogValueT keyValue;
|
||||||
|
CalogValueT stringValue;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
status = calogValueString(&keyValue, key, (int64_t)strlen(key));
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = calogValueString(&stringValue, bytes, length);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = calogAggSet(map, &keyValue, &stringValue);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
calogValueFree(&stringValue);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t httpParseInt(const char *bytes, size_t length) {
|
||||||
|
int64_t value;
|
||||||
|
size_t i;
|
||||||
|
bool any;
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
any = false;
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
if (bytes[i] < '0' || bytes[i] > '9') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
value = value * 10 + (int64_t)(bytes[i] - '0');
|
||||||
|
any = true;
|
||||||
|
if (value > (int64_t)HTTP_MAX_RESPONSE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpParseStatus(const char *line, size_t length, int32_t *codeOut) {
|
||||||
|
int32_t code;
|
||||||
|
size_t i;
|
||||||
|
bool any;
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
// Skip the "HTTP/x.y" token and the space(s) before the status code.
|
||||||
|
while (i < length && line[i] != ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (i < length && line[i] == ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
code = 0;
|
||||||
|
any = false;
|
||||||
|
while (i < length && line[i] >= '0' && line[i] <= '9') {
|
||||||
|
code = code * 10 + (int32_t)(line[i] - '0');
|
||||||
|
any = true;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
*codeOut = code;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpParseUrl(const char *url, int64_t urlLen, HttpUrlT *out, CalogValueT *result) {
|
||||||
|
size_t len;
|
||||||
|
size_t pos;
|
||||||
|
size_t hostStart;
|
||||||
|
size_t hostEnd;
|
||||||
|
|
||||||
|
len = (size_t)urlLen;
|
||||||
|
if (len >= 7 && memcmp(url, "http://", 7) == 0) {
|
||||||
|
out->https = false;
|
||||||
|
pos = 7;
|
||||||
|
} else if (len >= 8 && memcmp(url, "https://", 8) == 0) {
|
||||||
|
out->https = true;
|
||||||
|
pos = 8;
|
||||||
|
} else {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL must start with http:// or https://");
|
||||||
|
}
|
||||||
|
hostStart = pos;
|
||||||
|
while (pos < len && url[pos] != ':' && url[pos] != '/') {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
hostEnd = pos;
|
||||||
|
if (hostEnd == hostStart) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL has an empty host");
|
||||||
|
}
|
||||||
|
if (!httpCopyField(out->host, sizeof(out->host), url + hostStart, hostEnd - hostStart)) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL host too long");
|
||||||
|
}
|
||||||
|
if (pos < len && url[pos] == ':') {
|
||||||
|
size_t portStart;
|
||||||
|
pos++;
|
||||||
|
portStart = pos;
|
||||||
|
while (pos < len && url[pos] != '/') {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (!httpCopyField(out->port, sizeof(out->port), url + portStart, pos - portStart)) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL port too long");
|
||||||
|
}
|
||||||
|
if (out->port[0] == '\0') {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL has an empty port");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strcpy(out->port, out->https ? "443" : "80");
|
||||||
|
}
|
||||||
|
if (pos < len) {
|
||||||
|
if (!httpCopyField(out->path, sizeof(out->path), url + pos, len - pos)) {
|
||||||
|
return calogFail(result, calogErrArgE, "http: URL path too long");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strcpy(out->path, "/");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpPerform(const char *method, const char *urlBytes, int64_t urlLen, const CalogAggT *headers, const char *body, int64_t bodyLen, CalogValueT *result) {
|
||||||
|
HttpUrlT url;
|
||||||
|
HttpConnT conn;
|
||||||
|
HttpBufT request;
|
||||||
|
HttpBufT raw;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
calogValueNil(result);
|
||||||
|
request.bytes = NULL;
|
||||||
|
request.length = 0;
|
||||||
|
request.cap = 0;
|
||||||
|
raw.bytes = NULL;
|
||||||
|
raw.length = 0;
|
||||||
|
raw.cap = 0;
|
||||||
|
|
||||||
|
status = httpParseUrl(urlBytes, urlLen, &url, result);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request line + Host + Connection: close.
|
||||||
|
status = httpBufPutStr(&request, method);
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, " ");
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, url.path);
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, " HTTP/1.1\r\nHost: ");
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, url.host);
|
||||||
|
}
|
||||||
|
if (status == calogOkE && !httpDefaultPort(&url)) {
|
||||||
|
status = httpBufPutStr(&request, ":");
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, url.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status == calogOkE) {
|
||||||
|
status = httpBufPutStr(&request, "\r\nConnection: close\r\n");
|
||||||
|
}
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&request);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller-supplied headers.
|
||||||
|
if (headers != NULL) {
|
||||||
|
status = httpAppendHeaders(&request, headers);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&request);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-Length + the blank line + body.
|
||||||
|
if (bodyLen > 0) {
|
||||||
|
char lengthLine[64];
|
||||||
|
int n;
|
||||||
|
n = snprintf(lengthLine, sizeof(lengthLine), "Content-Length: %lld\r\n", (long long)bodyLen);
|
||||||
|
status = httpBufPutBytes(&request, lengthLine, (size_t)n);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&request);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = httpBufPutStr(&request, "\r\n");
|
||||||
|
if (status == calogOkE && bodyLen > 0) {
|
||||||
|
status = httpBufPutBytes(&request, body, (size_t)bodyLen);
|
||||||
|
}
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&request);
|
||||||
|
return calogFail(result, status, "http: out of memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
status = httpConnOpen(&conn, &url, result);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&request);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = httpConnWriteAll(&conn, request.bytes, request.length);
|
||||||
|
httpBufFree(&request);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpConnClose(&conn);
|
||||||
|
return calogFail(result, status, "http: failed to send request");
|
||||||
|
}
|
||||||
|
status = httpReadResponse(&conn, &raw);
|
||||||
|
httpConnClose(&conn);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
httpBufFree(&raw);
|
||||||
|
return calogFail(result, status, "http: failed to read response");
|
||||||
|
}
|
||||||
|
|
||||||
|
status = httpBuildResult((raw.bytes != NULL) ? raw.bytes : "", raw.length, result);
|
||||||
|
httpBufFree(&raw);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpReadResponse(HttpConnT *conn, HttpBufT *raw) {
|
||||||
|
char chunk[HTTP_READ_CHUNK];
|
||||||
|
|
||||||
|
// "Connection: close" means the server closes at end of message, so reading to EOF yields
|
||||||
|
// the entire response (head + body).
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n;
|
||||||
|
int32_t status;
|
||||||
|
n = httpConnRead(conn, chunk, sizeof(chunk));
|
||||||
|
if (n <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (raw->length + (size_t)n > (size_t)HTTP_MAX_RESPONSE) {
|
||||||
|
return calogErrRangeE;
|
||||||
|
}
|
||||||
|
status = httpBufPutBytes(raw, chunk, (size_t)n);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpRequestNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
CalogAggT *opts;
|
||||||
|
CalogValueT *urlValue;
|
||||||
|
CalogValueT *methodValue;
|
||||||
|
CalogValueT *headersValue;
|
||||||
|
CalogValueT *bodyValue;
|
||||||
|
CalogValueT keyValue;
|
||||||
|
const CalogAggT *headers;
|
||||||
|
const char *method;
|
||||||
|
const char *body;
|
||||||
|
int64_t bodyLen;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogAggE) {
|
||||||
|
return calogFail(result, calogErrArgE, "httpRequest expects (options)");
|
||||||
|
}
|
||||||
|
opts = args[0].as.agg;
|
||||||
|
|
||||||
|
// url (required).
|
||||||
|
status = calogValueString(&keyValue, "url", 3);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "httpRequest: out of memory");
|
||||||
|
}
|
||||||
|
urlValue = calogAggGet(opts, &keyValue);
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
if (urlValue == NULL || urlValue->type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "httpRequest: options.url must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// method (optional; default GET).
|
||||||
|
method = "GET";
|
||||||
|
status = calogValueString(&keyValue, "method", 6);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "httpRequest: out of memory");
|
||||||
|
}
|
||||||
|
methodValue = calogAggGet(opts, &keyValue);
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
if (methodValue != NULL && methodValue->type == calogStringE) {
|
||||||
|
method = methodValue->as.s.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// headers (optional).
|
||||||
|
headers = NULL;
|
||||||
|
status = calogValueString(&keyValue, "headers", 7);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "httpRequest: out of memory");
|
||||||
|
}
|
||||||
|
headersValue = calogAggGet(opts, &keyValue);
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
if (headersValue != NULL && headersValue->type == calogAggE) {
|
||||||
|
headers = headersValue->as.agg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// body (optional).
|
||||||
|
body = NULL;
|
||||||
|
bodyLen = 0;
|
||||||
|
status = calogValueString(&keyValue, "body", 4);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "httpRequest: out of memory");
|
||||||
|
}
|
||||||
|
bodyValue = calogAggGet(opts, &keyValue);
|
||||||
|
calogValueFree(&keyValue);
|
||||||
|
if (bodyValue != NULL && bodyValue->type == calogStringE) {
|
||||||
|
body = bodyValue->as.s.bytes;
|
||||||
|
bodyLen = bodyValue->as.s.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpPerform(method, urlValue->as.s.bytes, urlValue->as.s.length, headers, body, bodyLen, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t httpTlsHandshake(HttpConnT *conn, const HttpUrlT *url, CalogValueT *result) {
|
||||||
|
conn->ctx = SSL_CTX_new(TLS_client_method());
|
||||||
|
if (conn->ctx == NULL) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "http: TLS context creation failed");
|
||||||
|
}
|
||||||
|
conn->ssl = SSL_new(conn->ctx);
|
||||||
|
if (conn->ssl == NULL) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "http: TLS session creation failed");
|
||||||
|
}
|
||||||
|
if (SSL_set_fd(conn->ssl, conn->fd) != 1) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "http: TLS SSL_set_fd failed");
|
||||||
|
}
|
||||||
|
// SNI so name-based virtual hosts serve the right certificate. NOTE: v1 does NO certificate
|
||||||
|
// verification -- https here is transport encryption only, not server authentication.
|
||||||
|
(void)SSL_set_tlsext_host_name(conn->ssl, url->host);
|
||||||
|
if (SSL_connect(conn->ssl) != 1) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "http: TLS handshake failed");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
26
libs/calogHttp.h
Normal file
26
libs/calogHttp.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// calogHttp.h -- calog HTTP client library.
|
||||||
|
//
|
||||||
|
// Bridges calog scripts to a minimal HTTP/1.1 client so any engine can fetch and post over
|
||||||
|
// http:// and https:// without an engine-specific library:
|
||||||
|
// httpGet(url) -> { status, body, headers }
|
||||||
|
// httpRequest(opts) -> { status, body, headers }
|
||||||
|
// where opts is a map { method (default "GET"), url, headers (map of name->value), body }.
|
||||||
|
// The result map carries the integer status code, the binary-safe response body, and a
|
||||||
|
// headers map keyed by the lowercased header name. Each call opens its own connection
|
||||||
|
// ("Connection: close"), reads the whole response to EOF, and decodes the body honoring
|
||||||
|
// Transfer-Encoding: chunked / Content-Length / read-to-close. Redirects are NOT followed
|
||||||
|
// (a 3xx is returned as-is). For https:// the TLS handshake uses SNI but performs NO
|
||||||
|
// certificate verification (v1 -- treat https as transport encoding only, not authenticated).
|
||||||
|
// The natives are INLINE (each call is a self-contained connection with no shared state), so
|
||||||
|
// there is nothing to shut down.
|
||||||
|
|
||||||
|
#ifndef CALOG_HTTP_H
|
||||||
|
#define CALOG_HTTP_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the HTTP natives (httpGet, httpRequest) on a runtime. Stateless: safe to call on
|
||||||
|
// any number of runtimes, and there is no matching shutdown.
|
||||||
|
int32_t calogHttpRegister(CalogT *calog);
|
||||||
|
|
||||||
|
#endif
|
||||||
762
libs/calogJson.c
Normal file
762
libs/calogJson.c
Normal file
|
|
@ -0,0 +1,762 @@
|
||||||
|
// calogJson.c -- calog JSON library (see calogJson.h). A self-contained recursive-descent
|
||||||
|
// parser and a serializer, both bridging JSON text and calog's value/aggregate model. No
|
||||||
|
// shared state: the two natives are pure functions of their arguments.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogJson.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define JSON_BUF_INITIAL 64
|
||||||
|
|
||||||
|
// A growable output byte buffer for serialization and for decoded string bodies.
|
||||||
|
typedef struct JsonBufT {
|
||||||
|
char *bytes;
|
||||||
|
size_t length;
|
||||||
|
size_t cap;
|
||||||
|
} JsonBufT;
|
||||||
|
|
||||||
|
// A read cursor over the source text (binary-safe: bounded by end, not a NUL).
|
||||||
|
typedef struct JsonParseT {
|
||||||
|
const char *cur;
|
||||||
|
const char *end;
|
||||||
|
} JsonParseT;
|
||||||
|
|
||||||
|
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra);
|
||||||
|
static void jsonBufFree(JsonBufT *buf);
|
||||||
|
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length);
|
||||||
|
static int32_t jsonBufPutChar(JsonBufT *buf, char c);
|
||||||
|
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth);
|
||||||
|
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth);
|
||||||
|
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key);
|
||||||
|
static int32_t jsonEncodeReal(JsonBufT *buf, double r);
|
||||||
|
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length);
|
||||||
|
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out);
|
||||||
|
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth);
|
||||||
|
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out);
|
||||||
|
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out);
|
||||||
|
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth);
|
||||||
|
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out);
|
||||||
|
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf);
|
||||||
|
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth);
|
||||||
|
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp);
|
||||||
|
static void jsonSkipWs(JsonParseT *p);
|
||||||
|
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogJsonRegister(CalogT *calog) {
|
||||||
|
calogRegisterInline(calog, "jsonParse", jsonParseNative, NULL);
|
||||||
|
calogRegisterInline(calog, "jsonStringify", jsonStringifyNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonBufEnsure(JsonBufT *buf, size_t extra) {
|
||||||
|
size_t need;
|
||||||
|
size_t cap;
|
||||||
|
char *grown;
|
||||||
|
|
||||||
|
need = buf->length + extra;
|
||||||
|
if (need <= buf->cap) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
cap = (buf->cap == 0) ? JSON_BUF_INITIAL : buf->cap;
|
||||||
|
while (cap < need) {
|
||||||
|
cap *= CALOG_GROWTH_FACTOR;
|
||||||
|
}
|
||||||
|
grown = (char *)realloc(buf->bytes, cap);
|
||||||
|
if (grown == NULL) {
|
||||||
|
return calogErrOomE;
|
||||||
|
}
|
||||||
|
buf->bytes = grown;
|
||||||
|
buf->cap = cap;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void jsonBufFree(JsonBufT *buf) {
|
||||||
|
free(buf->bytes);
|
||||||
|
buf->bytes = NULL;
|
||||||
|
buf->length = 0;
|
||||||
|
buf->cap = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonBufPutBytes(JsonBufT *buf, const char *bytes, size_t length) {
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
status = jsonBufEnsure(buf, length);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
memcpy(buf->bytes + buf->length, bytes, length);
|
||||||
|
buf->length += length;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonBufPutChar(JsonBufT *buf, char c) {
|
||||||
|
return jsonBufPutBytes(buf, &c, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonEncode(JsonBufT *buf, const CalogValueT *value, int32_t depth) {
|
||||||
|
char tmp[32];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
if (depth >= CALOG_MAX_DEPTH) {
|
||||||
|
return calogErrDepthE;
|
||||||
|
}
|
||||||
|
switch (value->type) {
|
||||||
|
case calogNilE:
|
||||||
|
return jsonBufPutBytes(buf, "null", 4);
|
||||||
|
case calogBoolE:
|
||||||
|
return value->as.b ? jsonBufPutBytes(buf, "true", 4) : jsonBufPutBytes(buf, "false", 5);
|
||||||
|
case calogIntE:
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "%lld", (long long)value->as.i);
|
||||||
|
return jsonBufPutBytes(buf, tmp, (size_t)n);
|
||||||
|
case calogRealE:
|
||||||
|
return jsonEncodeReal(buf, value->as.r);
|
||||||
|
case calogStringE:
|
||||||
|
return jsonEncodeString(buf, value->as.s.bytes, value->as.s.length);
|
||||||
|
case calogAggE:
|
||||||
|
return jsonEncodeAgg(buf, value->as.agg, depth);
|
||||||
|
case calogFnE:
|
||||||
|
return calogErrTypeE;
|
||||||
|
default:
|
||||||
|
return calogErrTypeE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonEncodeAgg(JsonBufT *buf, const CalogAggT *agg, int32_t depth) {
|
||||||
|
int64_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
// A map (or a hybrid that carries keyed pairs) serializes as a JSON object; a pure
|
||||||
|
// sequence serializes as a JSON array. In the hybrid case the array elements are emitted
|
||||||
|
// under their integer indices so nothing is silently dropped.
|
||||||
|
if (agg->pairCount > 0 || agg->kind == calogMapE) {
|
||||||
|
bool first;
|
||||||
|
first = true;
|
||||||
|
status = jsonBufPutChar(buf, '{');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
for (index = 0; index < agg->arrayCount; index++) {
|
||||||
|
char tmp[32];
|
||||||
|
int n;
|
||||||
|
if (!first) {
|
||||||
|
status = jsonBufPutChar(buf, ',');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "\"%lld\":", (long long)index);
|
||||||
|
status = jsonBufPutBytes(buf, tmp, (size_t)n);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = jsonEncode(buf, &agg->array[index], depth + 1);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index = 0; index < agg->pairCount; index++) {
|
||||||
|
if (!first) {
|
||||||
|
status = jsonBufPutChar(buf, ',');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
status = jsonEncodeKey(buf, &agg->pairs[index].key);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = jsonBufPutChar(buf, ':');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = jsonEncode(buf, &agg->pairs[index].value, depth + 1);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonBufPutChar(buf, '}');
|
||||||
|
}
|
||||||
|
status = jsonBufPutChar(buf, '[');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
for (index = 0; index < agg->arrayCount; index++) {
|
||||||
|
if (index > 0) {
|
||||||
|
status = jsonBufPutChar(buf, ',');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = jsonEncode(buf, &agg->array[index], depth + 1);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonBufPutChar(buf, ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonEncodeKey(JsonBufT *buf, const CalogValueT *key) {
|
||||||
|
char tmp[32];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
// JSON object keys must be strings; coerce a scalar key to its string form.
|
||||||
|
switch (key->type) {
|
||||||
|
case calogStringE:
|
||||||
|
return jsonEncodeString(buf, key->as.s.bytes, key->as.s.length);
|
||||||
|
case calogIntE:
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "\"%lld\"", (long long)key->as.i);
|
||||||
|
return jsonBufPutBytes(buf, tmp, (size_t)n);
|
||||||
|
case calogBoolE:
|
||||||
|
return key->as.b ? jsonBufPutBytes(buf, "\"true\"", 6) : jsonBufPutBytes(buf, "\"false\"", 7);
|
||||||
|
case calogRealE:
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "\"%.17g\"", key->as.r);
|
||||||
|
return jsonBufPutBytes(buf, tmp, (size_t)n);
|
||||||
|
default:
|
||||||
|
return calogErrTypeE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonEncodeReal(JsonBufT *buf, double r) {
|
||||||
|
char tmp[40];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
if (!isfinite(r)) {
|
||||||
|
return jsonBufPutBytes(buf, "null", 4);
|
||||||
|
}
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "%.15g", r);
|
||||||
|
if (strtod(tmp, NULL) != r) {
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "%.17g", r);
|
||||||
|
}
|
||||||
|
return jsonBufPutBytes(buf, tmp, (size_t)n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonEncodeString(JsonBufT *buf, const char *bytes, int64_t length) {
|
||||||
|
int64_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
status = jsonBufPutChar(buf, '"');
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
for (index = 0; index < length; index++) {
|
||||||
|
unsigned char c;
|
||||||
|
c = (unsigned char)bytes[index];
|
||||||
|
switch (c) {
|
||||||
|
case '"':
|
||||||
|
status = jsonBufPutBytes(buf, "\\\"", 2);
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
status = jsonBufPutBytes(buf, "\\\\", 2);
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
status = jsonBufPutBytes(buf, "\\b", 2);
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
status = jsonBufPutBytes(buf, "\\f", 2);
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
status = jsonBufPutBytes(buf, "\\n", 2);
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
status = jsonBufPutBytes(buf, "\\r", 2);
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
status = jsonBufPutBytes(buf, "\\t", 2);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
char esc[8];
|
||||||
|
int n;
|
||||||
|
n = snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)c);
|
||||||
|
status = jsonBufPutBytes(buf, esc, (size_t)n);
|
||||||
|
} else {
|
||||||
|
status = jsonBufPutChar(buf, (char)c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonBufPutChar(buf, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonHexQuad(JsonParseT *p, uint32_t *out) {
|
||||||
|
uint32_t value;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
int digit;
|
||||||
|
if (p->cur >= p->end) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (*p->cur >= '0' && *p->cur <= '9') {
|
||||||
|
digit = *p->cur - '0';
|
||||||
|
} else if (*p->cur >= 'a' && *p->cur <= 'f') {
|
||||||
|
digit = *p->cur - 'a' + 10;
|
||||||
|
} else if (*p->cur >= 'A' && *p->cur <= 'F') {
|
||||||
|
digit = *p->cur - 'A' + 10;
|
||||||
|
} else {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
value = (value << 4) | (uint32_t)digit;
|
||||||
|
p->cur++;
|
||||||
|
}
|
||||||
|
*out = value;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseArray(JsonParseT *p, CalogValueT *out, int32_t depth) {
|
||||||
|
CalogAggT *agg;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
calogValueNil(out);
|
||||||
|
status = calogAggCreate(&agg, calogListE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
p->cur++; // consume '['
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur < p->end && *p->cur == ']') {
|
||||||
|
p->cur++;
|
||||||
|
calogValueAgg(out, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
CalogValueT element;
|
||||||
|
status = jsonParseValue(p, &element, depth + 1);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = calogAggPush(agg, &element);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&element);
|
||||||
|
calogAggFree(agg);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur >= p->end) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (*p->cur == ',') {
|
||||||
|
p->cur++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p->cur == ']') {
|
||||||
|
p->cur++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
calogValueAgg(out, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseLiteral(JsonParseT *p, CalogValueT *out) {
|
||||||
|
size_t remaining;
|
||||||
|
|
||||||
|
calogValueNil(out);
|
||||||
|
remaining = (size_t)(p->end - p->cur);
|
||||||
|
if (remaining >= 4 && memcmp(p->cur, "true", 4) == 0) {
|
||||||
|
calogValueBool(out, true);
|
||||||
|
p->cur += 4;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
if (remaining >= 5 && memcmp(p->cur, "false", 5) == 0) {
|
||||||
|
calogValueBool(out, false);
|
||||||
|
p->cur += 5;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
if (remaining >= 4 && memcmp(p->cur, "null", 4) == 0) {
|
||||||
|
calogValueNil(out);
|
||||||
|
p->cur += 4;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
JsonParseT parse;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "jsonParse expects (text)");
|
||||||
|
}
|
||||||
|
parse.cur = args[0].as.s.bytes;
|
||||||
|
parse.end = args[0].as.s.bytes + args[0].as.s.length;
|
||||||
|
status = jsonParseValue(&parse, result, 0);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(result);
|
||||||
|
return calogFail(result, status, "jsonParse: invalid JSON");
|
||||||
|
}
|
||||||
|
jsonSkipWs(&parse);
|
||||||
|
if (parse.cur != parse.end) {
|
||||||
|
calogValueFree(result);
|
||||||
|
return calogFail(result, calogErrArgE, "jsonParse: trailing characters after JSON value");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseNumber(JsonParseT *p, CalogValueT *out) {
|
||||||
|
const char *start;
|
||||||
|
char *stop;
|
||||||
|
bool isReal;
|
||||||
|
|
||||||
|
calogValueNil(out);
|
||||||
|
start = p->cur;
|
||||||
|
isReal = false;
|
||||||
|
if (p->cur < p->end && *p->cur == '-') {
|
||||||
|
p->cur++;
|
||||||
|
}
|
||||||
|
while (p->cur < p->end) {
|
||||||
|
char c;
|
||||||
|
c = *p->cur;
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
p->cur++;
|
||||||
|
} else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') {
|
||||||
|
isReal = true;
|
||||||
|
p->cur++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p->cur == start) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (isReal) {
|
||||||
|
double r;
|
||||||
|
r = strtod(start, &stop);
|
||||||
|
if (stop != p->cur) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
calogValueReal(out, r);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
long long i;
|
||||||
|
errno = 0;
|
||||||
|
i = strtoll(start, &stop, 10);
|
||||||
|
if (stop != p->cur) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (errno == ERANGE) {
|
||||||
|
// An integer literal that overflows int64 becomes a real, rather than silently
|
||||||
|
// saturating to LLONG_MAX/MIN.
|
||||||
|
double r;
|
||||||
|
r = strtod(start, &stop);
|
||||||
|
if (stop != p->cur) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
calogValueReal(out, r);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
calogValueInt(out, (int64_t)i);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseObject(JsonParseT *p, CalogValueT *out, int32_t depth) {
|
||||||
|
CalogAggT *agg;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
calogValueNil(out);
|
||||||
|
status = calogAggCreate(&agg, calogMapE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
p->cur++; // consume '{'
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur < p->end && *p->cur == '}') {
|
||||||
|
p->cur++;
|
||||||
|
calogValueAgg(out, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
CalogValueT key;
|
||||||
|
CalogValueT value;
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur >= p->end || *p->cur != '"') {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
status = jsonParseString(p, &key);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur >= p->end || *p->cur != ':') {
|
||||||
|
calogValueFree(&key);
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
p->cur++; // consume ':'
|
||||||
|
status = jsonParseValue(p, &value, depth + 1);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&key);
|
||||||
|
calogAggFree(agg);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
status = calogAggSet(agg, &key, &value);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&key);
|
||||||
|
calogValueFree(&value);
|
||||||
|
calogAggFree(agg);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur >= p->end) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (*p->cur == ',') {
|
||||||
|
p->cur++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p->cur == '}') {
|
||||||
|
p->cur++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
calogValueAgg(out, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseString(JsonParseT *p, CalogValueT *out) {
|
||||||
|
JsonBufT buf;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
calogValueNil(out);
|
||||||
|
buf.bytes = NULL;
|
||||||
|
buf.length = 0;
|
||||||
|
buf.cap = 0;
|
||||||
|
p->cur++; // consume opening '"'
|
||||||
|
while (p->cur < p->end) {
|
||||||
|
unsigned char c;
|
||||||
|
c = (unsigned char)*p->cur;
|
||||||
|
if (c == '"') {
|
||||||
|
p->cur++;
|
||||||
|
status = calogValueString(out, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (c == '\\') {
|
||||||
|
char e;
|
||||||
|
p->cur++;
|
||||||
|
if (p->cur >= p->end) {
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
e = *p->cur;
|
||||||
|
switch (e) {
|
||||||
|
case '"':
|
||||||
|
status = jsonBufPutChar(&buf, '"');
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
status = jsonBufPutChar(&buf, '\\');
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
status = jsonBufPutChar(&buf, '/');
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
status = jsonBufPutChar(&buf, '\b');
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
status = jsonBufPutChar(&buf, '\f');
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
status = jsonBufPutChar(&buf, '\n');
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
status = jsonBufPutChar(&buf, '\r');
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
status = jsonBufPutChar(&buf, '\t');
|
||||||
|
break;
|
||||||
|
case 'u':
|
||||||
|
status = jsonParseUnicode(p, &buf);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
if (status != calogOkE) {
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (e != 'u') {
|
||||||
|
p->cur++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c < 0x20) {
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
status = jsonBufPutChar(&buf, (char)c);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
p->cur++;
|
||||||
|
}
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseUnicode(JsonParseT *p, JsonBufT *buf) {
|
||||||
|
uint32_t cp;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
p->cur++; // consume 'u'
|
||||||
|
status = jsonHexQuad(p, &cp);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (cp >= 0xD800 && cp <= 0xDBFF) {
|
||||||
|
uint32_t lo;
|
||||||
|
if (p->cur + 1 >= p->end || p->cur[0] != '\\' || p->cur[1] != 'u') {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
p->cur += 2; // consume "\u"
|
||||||
|
status = jsonHexQuad(p, &lo);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
if (lo < 0xDC00 || lo > 0xDFFF) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
|
||||||
|
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
|
||||||
|
return calogErrArgE; // an unpaired low surrogate is not a valid code point
|
||||||
|
}
|
||||||
|
return jsonPutUtf8(buf, cp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonParseValue(JsonParseT *p, CalogValueT *out, int32_t depth) {
|
||||||
|
calogValueNil(out);
|
||||||
|
if (depth >= CALOG_MAX_DEPTH) {
|
||||||
|
return calogErrDepthE;
|
||||||
|
}
|
||||||
|
jsonSkipWs(p);
|
||||||
|
if (p->cur >= p->end) {
|
||||||
|
return calogErrArgE;
|
||||||
|
}
|
||||||
|
switch (*p->cur) {
|
||||||
|
case '{':
|
||||||
|
return jsonParseObject(p, out, depth);
|
||||||
|
case '[':
|
||||||
|
return jsonParseArray(p, out, depth);
|
||||||
|
case '"':
|
||||||
|
return jsonParseString(p, out);
|
||||||
|
case 't':
|
||||||
|
case 'f':
|
||||||
|
case 'n':
|
||||||
|
return jsonParseLiteral(p, out);
|
||||||
|
default:
|
||||||
|
return jsonParseNumber(p, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonPutUtf8(JsonBufT *buf, uint32_t cp) {
|
||||||
|
char bytes[4];
|
||||||
|
size_t count;
|
||||||
|
|
||||||
|
if (cp < 0x80) {
|
||||||
|
bytes[0] = (char)cp;
|
||||||
|
count = 1;
|
||||||
|
} else if (cp < 0x800) {
|
||||||
|
bytes[0] = (char)(0xC0 | (cp >> 6));
|
||||||
|
bytes[1] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
count = 2;
|
||||||
|
} else if (cp < 0x10000) {
|
||||||
|
bytes[0] = (char)(0xE0 | (cp >> 12));
|
||||||
|
bytes[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
bytes[2] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
count = 3;
|
||||||
|
} else {
|
||||||
|
bytes[0] = (char)(0xF0 | (cp >> 18));
|
||||||
|
bytes[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
bytes[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
bytes[3] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
count = 4;
|
||||||
|
}
|
||||||
|
return jsonBufPutBytes(buf, bytes, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void jsonSkipWs(JsonParseT *p) {
|
||||||
|
while (p->cur < p->end) {
|
||||||
|
char c;
|
||||||
|
c = *p->cur;
|
||||||
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
||||||
|
p->cur++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t jsonStringifyNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
JsonBufT buf;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1) {
|
||||||
|
return calogFail(result, calogErrArgE, "jsonStringify expects (value)");
|
||||||
|
}
|
||||||
|
buf.bytes = NULL;
|
||||||
|
buf.length = 0;
|
||||||
|
buf.cap = 0;
|
||||||
|
status = jsonEncode(&buf, &args[0], 0);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return calogFail(result, status, "jsonStringify: value is not JSON-serializable");
|
||||||
|
}
|
||||||
|
status = calogValueString(result, (buf.bytes != NULL) ? buf.bytes : "", (int64_t)buf.length);
|
||||||
|
jsonBufFree(&buf);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
21
libs/calogJson.h
Normal file
21
libs/calogJson.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// calogJson.h -- calog JSON library.
|
||||||
|
//
|
||||||
|
// Bridges JSON text and calog's canonical value/aggregate model, so any engine can parse
|
||||||
|
// and produce JSON without an engine-specific library:
|
||||||
|
// jsonParse(text) -> value (object -> map, array -> list, number ->
|
||||||
|
// int or real, string, true/false, null -> nil)
|
||||||
|
// jsonStringify(value) -> compact JSON text
|
||||||
|
// Marshalling is binary-safe; \uXXXX (with surrogate pairs) decodes to UTF-8. Nesting is
|
||||||
|
// bounded by CALOG_MAX_DEPTH both ways. The natives are INLINE (pure computation, no shared
|
||||||
|
// state), so there is nothing to shut down.
|
||||||
|
|
||||||
|
#ifndef CALOG_JSON_H
|
||||||
|
#define CALOG_JSON_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the JSON natives (jsonParse, jsonStringify) on a runtime. Stateless: safe to call
|
||||||
|
// on any number of runtimes, and there is no matching shutdown.
|
||||||
|
int32_t calogJsonRegister(CalogT *calog);
|
||||||
|
|
||||||
|
#endif
|
||||||
286
libs/calogKv.c
Normal file
286
libs/calogKv.c
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
// calogKv.c -- calog key-value library (see calogKv.h). A process-wide, mutex-guarded array
|
||||||
|
// of {key bytes, key length, value}. kvSet/kvGet/kvHas/kvDelete/kvKeys are natives over that
|
||||||
|
// shared store; every stored value is a deep copy, so the store owns pure data.
|
||||||
|
//
|
||||||
|
// The registry state is STATIC (not heap) and its bookkeeping is never freed: the five
|
||||||
|
// natives stay reachable (via any context) right up until calogDestroy tears the broker
|
||||||
|
// registry down, so freeing the state in calogKvShutdown -- which the contract requires
|
||||||
|
// BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown
|
||||||
|
// instead just frees the stored keys + values and empties the array; the small static
|
||||||
|
// bookkeeping persists for the process (no per-cycle leak), and a post-shutdown native call
|
||||||
|
// simply sees an empty store.
|
||||||
|
//
|
||||||
|
// Function values are refused (calogErrUnsupportedE): storing a CalogFnT would drag script
|
||||||
|
// callable lifecycle into a data store, so kv sidesteps it entirely and holds data only.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogKv.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define KV_INITIAL_CAP 8
|
||||||
|
#define KV_GROWTH 2
|
||||||
|
|
||||||
|
typedef struct KvEntryT {
|
||||||
|
char *keyBytes;
|
||||||
|
int64_t keyLen;
|
||||||
|
CalogValueT value;
|
||||||
|
} KvEntryT;
|
||||||
|
|
||||||
|
// The store (heap array), guarded by gMapMutex. refCount (guarded by gInitMutex) counts
|
||||||
|
// registered runtimes so the LAST calogKvShutdown frees the store's contents.
|
||||||
|
static pthread_mutex_t gMapMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static KvEntryT *gEntries = NULL;
|
||||||
|
static int32_t gCount = 0;
|
||||||
|
static int32_t gCap = 0;
|
||||||
|
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static int32_t gRefCount = 0;
|
||||||
|
|
||||||
|
static bool kvContainsFn(const CalogValueT *value, int32_t depth);
|
||||||
|
static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t kvFindLocked(const char *bytes, int64_t length);
|
||||||
|
static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogKvRegister(CalogT *calog) {
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
gRefCount++;
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
calogRegisterInline(calog, "kvSet", kvSet, NULL);
|
||||||
|
calogRegisterInline(calog, "kvGet", kvGet, NULL);
|
||||||
|
calogRegisterInline(calog, "kvHas", kvHas, NULL);
|
||||||
|
calogRegisterInline(calog, "kvDelete", kvDelete, NULL);
|
||||||
|
calogRegisterInline(calog, "kvKeys", kvKeys, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void calogKvShutdown(void) {
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
if (gRefCount > 0) {
|
||||||
|
gRefCount--;
|
||||||
|
}
|
||||||
|
if (gRefCount <= 0) {
|
||||||
|
int32_t index;
|
||||||
|
pthread_mutex_lock(&gMapMutex);
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
free(gEntries[index].keyBytes);
|
||||||
|
calogValueFree(&gEntries[index].value);
|
||||||
|
}
|
||||||
|
free(gEntries);
|
||||||
|
gEntries = NULL;
|
||||||
|
gCount = 0;
|
||||||
|
gCap = 0;
|
||||||
|
pthread_mutex_unlock(&gMapMutex);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool kvContainsFn(const CalogValueT *value, int32_t depth) {
|
||||||
|
int64_t index;
|
||||||
|
|
||||||
|
// kv stores only data (copied by value); a function value -- at the top level OR nested in
|
||||||
|
// an aggregate -- would retain a CalogFnT into the process-wide store past its owner's life,
|
||||||
|
// so reject any value that carries one. Too-deep is rejected as unverifiable.
|
||||||
|
if (depth >= CALOG_MAX_DEPTH) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value->type == calogFnE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value->type == calogAggE) {
|
||||||
|
for (index = 0; index < value->as.agg->arrayCount; index++) {
|
||||||
|
if (kvContainsFn(&value->as.agg->array[index], depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index = 0; index < value->as.agg->pairCount; index++) {
|
||||||
|
if (kvContainsFn(&value->as.agg->pairs[index].key, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (kvContainsFn(&value->as.agg->pairs[index].value, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "kvDelete expects (key)");
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&gMapMutex);
|
||||||
|
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
|
||||||
|
if (index >= 0) {
|
||||||
|
free(gEntries[index].keyBytes);
|
||||||
|
calogValueFree(&gEntries[index].value);
|
||||||
|
gEntries[index] = gEntries[gCount - 1];
|
||||||
|
gCount--;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gMapMutex);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvFindLocked(const char *bytes, int64_t length) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
if (gEntries[index].keyLen == length && memcmp(gEntries[index].keyBytes, bytes, (size_t)length) == 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int32_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "kvGet expects (key)");
|
||||||
|
}
|
||||||
|
status = calogOkE;
|
||||||
|
pthread_mutex_lock(&gMapMutex);
|
||||||
|
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
|
||||||
|
if (index >= 0) {
|
||||||
|
status = calogValueCopy(result, &gEntries[index].value);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gMapMutex);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(result);
|
||||||
|
return calogFail(result, status, "kvGet: out of memory");
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "kvHas expects (key)");
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&gMapMutex);
|
||||||
|
index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
|
||||||
|
pthread_mutex_unlock(&gMapMutex);
|
||||||
|
calogValueBool(result, index >= 0);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
CalogAggT *agg;
|
||||||
|
int32_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
status = calogAggCreate(&agg, calogListE);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
return calogFail(result, status, "kvKeys: out of memory");
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&gMapMutex);
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
CalogValueT key;
|
||||||
|
status = calogValueString(&key, gEntries[index].keyBytes, gEntries[index].keyLen);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
status = calogAggPush(agg, &key);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gMapMutex);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogAggFree(agg);
|
||||||
|
return calogFail(result, status, "kvKeys: out of memory");
|
||||||
|
}
|
||||||
|
calogValueAgg(result, agg);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
CalogValueT copy;
|
||||||
|
char *keyBytes;
|
||||||
|
int64_t keyLen;
|
||||||
|
int32_t index;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "kvSet expects (key, value)");
|
||||||
|
}
|
||||||
|
if (kvContainsFn(&args[1], 0)) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "kv stores data, not functions");
|
||||||
|
}
|
||||||
|
status = calogValueCopy(©, &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;
|
||||||
|
}
|
||||||
30
libs/calogKv.h
Normal file
30
libs/calogKv.h
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// calogKv.h -- calog key-value library: a process-wide, thread-safe data store.
|
||||||
|
//
|
||||||
|
// Registers natives so any script, on any engine, can stash and fetch values under a
|
||||||
|
// string key in one shared store:
|
||||||
|
// kvSet(key, value) store a DEEP COPY of value under key (replaces any existing) -> nil
|
||||||
|
// kvGet(key) -> a deep copy of the stored value, or nil if the key is absent
|
||||||
|
// kvHas(key) -> true if the key is present, else false
|
||||||
|
// kvDelete(key) remove the key (no error if it is absent) -> nil
|
||||||
|
// kvKeys() -> a list of the stored keys (each a string)
|
||||||
|
//
|
||||||
|
// The store holds DATA ONLY: kvSet rejects a function value (calogFnE) so no script
|
||||||
|
// callable's lifecycle leaks into the shared store -- kv keeps only values copied by
|
||||||
|
// value. Keys are binary-safe: they are compared by bytes + length, so embedded NULs are
|
||||||
|
// honored. The registry is process-wide and reference-counted across runtimes.
|
||||||
|
|
||||||
|
#ifndef CALOG_KV_H
|
||||||
|
#define CALOG_KV_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the kv natives on a runtime. Idempotent across runtimes (shared registry).
|
||||||
|
int32_t calogKvRegister(CalogT *calog);
|
||||||
|
|
||||||
|
// Free every stored key + value (once the last registered runtime unregisters). Like
|
||||||
|
// calogExportShutdown, call this BEFORE calogDestroy. The static registry bookkeeping is
|
||||||
|
// intentionally never freed -- the natives stay callable right up until calogDestroy tears
|
||||||
|
// the broker down -- so a native call after shutdown is safe (it just sees an empty store).
|
||||||
|
void calogKvShutdown(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
229
libs/calogPubsub.c
Normal file
229
libs/calogPubsub.c
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
// calogPubsub.c -- calog publish/subscribe library (see calogPubsub.h). A process-wide,
|
||||||
|
// mutex-guarded list of {int64 id, topic bytes, topic length, callback}. subscribe/
|
||||||
|
// unsubscribe/publish are natives over that shared list; subscribe retains the callback
|
||||||
|
// function value, and publish delivers a deep copy of the message to every matching
|
||||||
|
// subscriber SYNCHRONOUSLY.
|
||||||
|
//
|
||||||
|
// The registry state is STATIC (not heap) and its bookkeeping is never freed: the three
|
||||||
|
// natives stay reachable (via any context) right up until calogDestroy tears the broker
|
||||||
|
// registry down, so freeing the state in calogPubsubShutdown -- which the contract requires
|
||||||
|
// BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown
|
||||||
|
// instead releases every subscribed callback and empties the list; the small static
|
||||||
|
// bookkeeping persists for the process (no per-cycle leak), and a post-shutdown publish
|
||||||
|
// simply finds no subscribers.
|
||||||
|
//
|
||||||
|
// Delivery discipline: publish collects (and retains) the matching callbacks under the lock,
|
||||||
|
// UNLOCKS, then invokes each -- the mutex is NEVER held across calogFnInvoke, and the
|
||||||
|
// retained reference keeps a concurrently-unsubscribed callback alive across the call. Each
|
||||||
|
// callback marshals to its owner context's thread; a subscriber that publishes back onto a
|
||||||
|
// topic it is on can deadlock (delivery is synchronous), so keep publish graphs acyclic.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogPubsub.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define PUBSUB_INITIAL_CAP 8
|
||||||
|
#define PUBSUB_GROWTH 2
|
||||||
|
|
||||||
|
typedef struct SubEntryT {
|
||||||
|
int64_t id;
|
||||||
|
char *topic;
|
||||||
|
int64_t topicLen;
|
||||||
|
CalogFnT *callback;
|
||||||
|
} SubEntryT;
|
||||||
|
|
||||||
|
// The list (heap array), guarded by gListMutex. gNextId hands out monotonically increasing
|
||||||
|
// subscription ids. refCount (guarded by gInitMutex) counts registered runtimes so the LAST
|
||||||
|
// calogPubsubShutdown releases the callbacks and empties the list.
|
||||||
|
static pthread_mutex_t gListMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static SubEntryT *gEntries = NULL;
|
||||||
|
static int32_t gCount = 0;
|
||||||
|
static int32_t gCap = 0;
|
||||||
|
static int64_t gNextId = 1;
|
||||||
|
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static int32_t gRefCount = 0;
|
||||||
|
|
||||||
|
static int32_t pubsubFindByIdLocked(int64_t id);
|
||||||
|
static int32_t pubsubPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t pubsubSubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t pubsubUnsubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogPubsubRegister(CalogT *calog) {
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
gRefCount++;
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
calogRegisterInline(calog, "subscribe", pubsubSubscribe, NULL);
|
||||||
|
calogRegisterInline(calog, "unsubscribe", pubsubUnsubscribe, NULL);
|
||||||
|
calogRegisterInline(calog, "publish", pubsubPublish, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void calogPubsubShutdown(void) {
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
if (gRefCount > 0) {
|
||||||
|
gRefCount--;
|
||||||
|
}
|
||||||
|
if (gRefCount <= 0) {
|
||||||
|
int32_t index;
|
||||||
|
pthread_mutex_lock(&gListMutex);
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
free(gEntries[index].topic);
|
||||||
|
calogFnRelease(gEntries[index].callback);
|
||||||
|
}
|
||||||
|
free(gEntries);
|
||||||
|
gEntries = NULL;
|
||||||
|
gCount = 0;
|
||||||
|
gCap = 0;
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t pubsubFindByIdLocked(int64_t id) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
if (gEntries[index].id == id) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t pubsubPublish(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
CalogFnT **collected;
|
||||||
|
int64_t topicLen;
|
||||||
|
int32_t delivered;
|
||||||
|
int32_t matched;
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE) {
|
||||||
|
return calogFail(result, calogErrArgE, "publish expects (topic, message)");
|
||||||
|
}
|
||||||
|
topicLen = args[0].as.s.length;
|
||||||
|
collected = NULL;
|
||||||
|
matched = 0;
|
||||||
|
delivered = 0;
|
||||||
|
pthread_mutex_lock(&gListMutex);
|
||||||
|
if (gCount > 0) {
|
||||||
|
collected = (CalogFnT **)malloc((size_t)gCount * sizeof(CalogFnT *));
|
||||||
|
if (collected == NULL) {
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
return calogFail(result, calogErrOomE, "publish: out of memory");
|
||||||
|
}
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
if (gEntries[index].topicLen == topicLen && memcmp(gEntries[index].topic, args[0].as.s.bytes, (size_t)topicLen) == 0) {
|
||||||
|
// Retain across the (unlocked) delivery so a concurrent unsubscribe cannot free it.
|
||||||
|
calogFnRetain(gEntries[index].callback);
|
||||||
|
collected[matched] = gEntries[index].callback;
|
||||||
|
matched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
// Deliver OUTSIDE the lock: each callback marshals to its owner context's thread and may
|
||||||
|
// re-enter subscribe/unsubscribe/publish. The mutex is never held across calogFnInvoke.
|
||||||
|
for (index = 0; index < matched; index++) {
|
||||||
|
CalogValueT messageCopy;
|
||||||
|
CalogValueT callResult;
|
||||||
|
int32_t status;
|
||||||
|
status = calogValueCopy(&messageCopy, &args[1]);
|
||||||
|
if (status != calogOkE) {
|
||||||
|
calogValueFree(&messageCopy);
|
||||||
|
calogFnRelease(collected[index]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
calogValueNil(&callResult);
|
||||||
|
status = calogFnInvoke(collected[index], &messageCopy, 1, &callResult);
|
||||||
|
calogValueFree(&callResult);
|
||||||
|
calogValueFree(&messageCopy);
|
||||||
|
calogFnRelease(collected[index]);
|
||||||
|
// A subscriber whose owner context is gone was not actually invoked, so it is not
|
||||||
|
// counted (calogErrDeadE from the marshalled path, calogErrNotFoundE from the direct).
|
||||||
|
if (status != calogErrDeadE && status != calogErrNotFoundE) {
|
||||||
|
delivered++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(collected);
|
||||||
|
calogValueInt(result, (int64_t)delivered);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t pubsubSubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
char *topicCopy;
|
||||||
|
int64_t id;
|
||||||
|
int64_t topicLen;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogFnE) {
|
||||||
|
return calogFail(result, calogErrArgE, "subscribe expects (topic, function)");
|
||||||
|
}
|
||||||
|
topicLen = args[0].as.s.length;
|
||||||
|
topicCopy = (char *)malloc((size_t)topicLen + 1);
|
||||||
|
if (topicCopy == NULL) {
|
||||||
|
return calogFail(result, calogErrOomE, "subscribe: out of memory");
|
||||||
|
}
|
||||||
|
memcpy(topicCopy, args[0].as.s.bytes, (size_t)topicLen);
|
||||||
|
topicCopy[topicLen] = '\0';
|
||||||
|
pthread_mutex_lock(&gListMutex);
|
||||||
|
if (gCount == gCap) {
|
||||||
|
int32_t newCap;
|
||||||
|
SubEntryT *grown;
|
||||||
|
newCap = (gCap == 0) ? PUBSUB_INITIAL_CAP : gCap * PUBSUB_GROWTH;
|
||||||
|
grown = (SubEntryT *)realloc(gEntries, (size_t)newCap * sizeof(SubEntryT));
|
||||||
|
if (grown == NULL) {
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
free(topicCopy);
|
||||||
|
return calogFail(result, calogErrOomE, "subscribe: out of memory");
|
||||||
|
}
|
||||||
|
gEntries = grown;
|
||||||
|
gCap = newCap;
|
||||||
|
}
|
||||||
|
id = gNextId;
|
||||||
|
gNextId++;
|
||||||
|
calogFnRetain(args[1].as.fn);
|
||||||
|
gEntries[gCount].id = id;
|
||||||
|
gEntries[gCount].topic = topicCopy;
|
||||||
|
gEntries[gCount].topicLen = topicLen;
|
||||||
|
gEntries[gCount].callback = args[1].as.fn;
|
||||||
|
gCount++;
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
calogValueInt(result, id);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t pubsubUnsubscribe(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t id;
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "unsubscribe expects (id)");
|
||||||
|
}
|
||||||
|
id = (args[0].type == calogIntE) ? args[0].as.i : (int64_t)args[0].as.r;
|
||||||
|
pthread_mutex_lock(&gListMutex);
|
||||||
|
index = pubsubFindByIdLocked(id);
|
||||||
|
if (index >= 0) {
|
||||||
|
free(gEntries[index].topic);
|
||||||
|
calogFnRelease(gEntries[index].callback);
|
||||||
|
gEntries[index] = gEntries[gCount - 1];
|
||||||
|
gCount--;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gListMutex);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
41
libs/calogPubsub.h
Normal file
41
libs/calogPubsub.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// calogPubsub.h -- calog publish/subscribe library: deliver a message to every subscriber
|
||||||
|
// of a topic, across contexts and engines.
|
||||||
|
//
|
||||||
|
// Registers natives so a script can broadcast to any number of listeners by topic name:
|
||||||
|
// subscribe(topic, fn) -> id register fn to receive messages published on topic
|
||||||
|
// unsubscribe(id) -> nil drop the subscription with that id
|
||||||
|
// publish(topic, msg) -> count deliver a copy of msg to every subscriber of topic,
|
||||||
|
// returning how many subscribers were invoked
|
||||||
|
//
|
||||||
|
// A subscriber is an ordinary calog function value, so each delivery runs in the
|
||||||
|
// subscriber's OWN context/thread (marshalled like any callable) and receives its own deep
|
||||||
|
// copy of the message -- publishing binary-safe data or nested aggregates is fine. Topics are
|
||||||
|
// matched by exact bytes (binary-safe), so a topic may itself contain embedded NULs.
|
||||||
|
//
|
||||||
|
// Delivery is SYNCHRONOUS: publish does not return until every matching subscriber has run.
|
||||||
|
// Because a subscriber runs on its owner context's thread, a subscriber that (directly or
|
||||||
|
// transitively) publishes back onto a topic it is itself subscribed to can DEADLOCK -- the
|
||||||
|
// publishing thread blocks in the callback, which cannot complete until the first publish
|
||||||
|
// returns. Keep publish graphs acyclic. A publish after a subscriber's owner context is gone
|
||||||
|
// simply skips that subscriber and does not count it.
|
||||||
|
//
|
||||||
|
// The registry is process-wide and reference-counted across runtimes.
|
||||||
|
|
||||||
|
#ifndef CALOG_PUBSUB_H
|
||||||
|
#define CALOG_PUBSUB_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the pubsub natives (subscribe, unsubscribe, publish) on a runtime. Idempotent
|
||||||
|
// across runtimes (shared registry).
|
||||||
|
int32_t calogPubsubRegister(CalogT *calog);
|
||||||
|
|
||||||
|
// Release every subscribed function (once the last registered runtime unregisters). Like the
|
||||||
|
// export library, call this while the subscribing contexts are still ALIVE -- before you
|
||||||
|
// close them and before calogDestroy -- because a subscription is a live reference into its
|
||||||
|
// owner's interpreter; releasing it after that context is gone would touch freed memory. The
|
||||||
|
// static registry bookkeeping itself is intentionally never freed (the natives stay callable
|
||||||
|
// until calogDestroy), so this is safe to call and re-register across runtimes.
|
||||||
|
void calogPubsubShutdown(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
91
libs/calogTime.c
Normal file
91
libs/calogTime.c
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// calogTime.c -- calog time library (see calogTime.h). Three stateless natives over the
|
||||||
|
// POSIX clocks: a wall-clock read, a monotonic read, and a millisecond sleep. No shared
|
||||||
|
// state and no timers/threads -- each native is a pure function of its arguments (or a
|
||||||
|
// plain block of the calling context thread).
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogTime.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static int32_t timeMonotonicNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t timeNowNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t timeSleepNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static double timeToSeconds(const struct timespec *ts);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogTimeRegister(CalogT *calog) {
|
||||||
|
calogRegisterInline(calog, "timeNow", timeNowNative, NULL);
|
||||||
|
calogRegisterInline(calog, "timeMonotonic", timeMonotonicNative, NULL);
|
||||||
|
calogRegisterInline(calog, "timeSleep", timeSleepNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timeMonotonicNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct timespec ts;
|
||||||
|
|
||||||
|
(void)args;
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 0) {
|
||||||
|
return calogFail(result, calogErrArgE, "timeMonotonic expects ()");
|
||||||
|
}
|
||||||
|
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "timeMonotonic: clock_gettime failed");
|
||||||
|
}
|
||||||
|
calogValueReal(result, timeToSeconds(&ts));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timeNowNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct timespec ts;
|
||||||
|
|
||||||
|
(void)args;
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 0) {
|
||||||
|
return calogFail(result, calogErrArgE, "timeNow expects ()");
|
||||||
|
}
|
||||||
|
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "timeNow: clock_gettime failed");
|
||||||
|
}
|
||||||
|
calogValueReal(result, timeToSeconds(&ts));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timeSleepNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
struct timespec request;
|
||||||
|
struct timespec remaining;
|
||||||
|
double ms;
|
||||||
|
int64_t nanos;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "timeSleep expects (milliseconds)");
|
||||||
|
}
|
||||||
|
ms = (args[0].type == calogIntE) ? (double)args[0].as.i : args[0].as.r;
|
||||||
|
if (!(ms >= 0.0)) {
|
||||||
|
return calogFail(result, calogErrRangeE, "timeSleep: milliseconds must be non-negative");
|
||||||
|
}
|
||||||
|
nanos = (int64_t)(ms * 1000000.0);
|
||||||
|
request.tv_sec = (time_t)(nanos / 1000000000);
|
||||||
|
request.tv_nsec = (long)(nanos % 1000000000);
|
||||||
|
while (nanosleep(&request, &remaining) != 0) {
|
||||||
|
if (errno != EINTR) {
|
||||||
|
return calogFail(result, calogErrUnsupportedE, "timeSleep: nanosleep failed");
|
||||||
|
}
|
||||||
|
request = remaining;
|
||||||
|
}
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static double timeToSeconds(const struct timespec *ts) {
|
||||||
|
return (double)ts->tv_sec + (double)ts->tv_nsec / 1000000000.0;
|
||||||
|
}
|
||||||
20
libs/calogTime.h
Normal file
20
libs/calogTime.h
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// calogTime.h -- calog time library.
|
||||||
|
//
|
||||||
|
// A tiny, stateless clock + sleep bridge, so any engine can read the wall-clock and
|
||||||
|
// monotonic clocks and block for a while without an engine-specific library:
|
||||||
|
// timeNow() -> real (epoch seconds, fractional; CLOCK_REALTIME)
|
||||||
|
// timeMonotonic() -> real (seconds from an unspecified origin; CLOCK_MONOTONIC)
|
||||||
|
// timeSleep(ms) -> nil (block the calling context for ms milliseconds)
|
||||||
|
// The natives are INLINE (pure clock reads / a plain blocking sleep, no shared state, no
|
||||||
|
// timers or threads), so there is nothing to shut down.
|
||||||
|
|
||||||
|
#ifndef CALOG_TIME_H
|
||||||
|
#define CALOG_TIME_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the time natives (timeNow, timeMonotonic, timeSleep) on a runtime. Stateless:
|
||||||
|
// safe to call on any number of runtimes, and there is no matching shutdown.
|
||||||
|
int32_t calogTimeRegister(CalogT *calog);
|
||||||
|
|
||||||
|
#endif
|
||||||
350
libs/calogTimer.c
Normal file
350
libs/calogTimer.c
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
// calogTimer.c -- calog timer library (see calogTimer.h). ONE background thread owns a
|
||||||
|
// mutex-guarded list of timers and, for each one that comes due, retains its callback, drops
|
||||||
|
// the list lock, and invokes it (which marshals to the callback's owning context thread). The
|
||||||
|
// list is scheduled on CLOCK_MONOTONIC; the thread sleeps on a condition variable (given the
|
||||||
|
// MONOTONIC clock via a condattr) until the earliest nextFire, or until it is signalled when a
|
||||||
|
// timer is added or cancelled.
|
||||||
|
//
|
||||||
|
// The registry state is STATIC and its small bookkeeping is never freed: the three natives
|
||||||
|
// stay reachable (via any context) right up until calogDestroy tears the broker registry down,
|
||||||
|
// so freeing the state in calogTimerShutdown -- which the contract requires BEFORE calogDestroy
|
||||||
|
// -- would leave those natives dereferencing freed memory. Shutdown instead stops the thread,
|
||||||
|
// releases every remaining callback, and empties the list; the mutexes/counters persist for the
|
||||||
|
// process (no per-cycle leak), so the library re-registers cleanly across runtimes.
|
||||||
|
//
|
||||||
|
// Locking discipline: the list mutex is NEVER held across calogFnInvoke (a blocking, cross-
|
||||||
|
// thread call) -- the callback is retained, the lock dropped, then the callback invoked and
|
||||||
|
// released. calogFnRelease is only ever held under the list mutex to post a non-blocking
|
||||||
|
// finalize (as in calogExport); it never re-enters the timer library.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calogTimer.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define TIMER_INITIAL_CAP 8
|
||||||
|
#define TIMER_GROWTH 2
|
||||||
|
|
||||||
|
// One scheduled timer. intervalNs == 0 marks a one-shot; a periodic timer keeps a non-zero
|
||||||
|
// interval. A cancelled timer is tombstoned here and pruned by the thread at the top of its
|
||||||
|
// loop, which keeps cancellation safe against a fire that is already in flight.
|
||||||
|
typedef struct TimerEntryT {
|
||||||
|
int64_t id;
|
||||||
|
CalogFnT *callback;
|
||||||
|
int64_t nextFireMonoNs;
|
||||||
|
int64_t intervalNs;
|
||||||
|
bool cancelled;
|
||||||
|
} TimerEntryT;
|
||||||
|
|
||||||
|
// The timer list (heap array) plus its background thread, guarded by gTimerMutex/gTimerCond.
|
||||||
|
// refCount (guarded by gInitMutex) counts registered runtimes so the LAST calogTimerShutdown
|
||||||
|
// stops the thread and frees the list. gTimerCond is initialised (with CLOCK_MONOTONIC) when
|
||||||
|
// the thread starts and destroyed when it stops, so every cond operation is guarded by
|
||||||
|
// gThreadStarted.
|
||||||
|
static pthread_mutex_t gTimerMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static pthread_cond_t gTimerCond;
|
||||||
|
static TimerEntryT *gTimers = NULL;
|
||||||
|
static int32_t gCount = 0;
|
||||||
|
static int32_t gCap = 0;
|
||||||
|
static int64_t gNextId = 1;
|
||||||
|
static pthread_t gThread;
|
||||||
|
static bool gThreadStarted = false;
|
||||||
|
static bool gShutdown = false;
|
||||||
|
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static int32_t gRefCount = 0;
|
||||||
|
|
||||||
|
static int32_t timerAfterNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t timerCancelNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t timerEnsureThreadLocked(void);
|
||||||
|
static int32_t timerEveryNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t timerFindByIdLocked(int64_t id);
|
||||||
|
static int64_t timerNowNs(void);
|
||||||
|
static void timerPruneLocked(void);
|
||||||
|
static int32_t timerSchedule(CalogValueT *args, int32_t argCount, CalogValueT *result, bool periodic);
|
||||||
|
static void *timerThreadMain(void *arg);
|
||||||
|
|
||||||
|
|
||||||
|
int32_t calogTimerRegister(CalogT *calog) {
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
gRefCount++;
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
calogRegisterInline(calog, "timerAfter", timerAfterNative, NULL);
|
||||||
|
calogRegisterInline(calog, "timerEvery", timerEveryNative, NULL);
|
||||||
|
calogRegisterInline(calog, "timerCancel", timerCancelNative, NULL);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void calogTimerShutdown(void) {
|
||||||
|
pthread_t threadHandle;
|
||||||
|
bool joinThread;
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
joinThread = false;
|
||||||
|
pthread_mutex_lock(&gInitMutex);
|
||||||
|
if (gRefCount > 0) {
|
||||||
|
gRefCount--;
|
||||||
|
}
|
||||||
|
if (gRefCount > 0) {
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Last runtime is gone: stop the thread (if it ever started), then release every remaining
|
||||||
|
// callback and free the list. Contexts are still alive here, so a callback release routes a
|
||||||
|
// finalize to its owner thread just like calogExport.
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
gShutdown = true;
|
||||||
|
threadHandle = gThread;
|
||||||
|
if (gThreadStarted) {
|
||||||
|
joinThread = true;
|
||||||
|
pthread_cond_signal(&gTimerCond);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
if (joinThread) {
|
||||||
|
pthread_join(threadHandle, NULL);
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
calogFnRelease(gTimers[index].callback);
|
||||||
|
}
|
||||||
|
free(gTimers);
|
||||||
|
gTimers = NULL;
|
||||||
|
gCount = 0;
|
||||||
|
gCap = 0;
|
||||||
|
if (joinThread) {
|
||||||
|
pthread_cond_destroy(&gTimerCond);
|
||||||
|
}
|
||||||
|
gThreadStarted = false;
|
||||||
|
gShutdown = false;
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
pthread_mutex_unlock(&gInitMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerAfterNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
return timerSchedule(args, argCount, result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerCancelNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 1 || args[0].type != calogIntE) {
|
||||||
|
return calogFail(result, calogErrArgE, "timerCancel expects (id)");
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
index = timerFindByIdLocked(args[0].as.i);
|
||||||
|
if (index >= 0) {
|
||||||
|
// Tombstone it; the thread prunes it (and stops firing it) at the top of its loop.
|
||||||
|
gTimers[index].cancelled = true;
|
||||||
|
pthread_cond_signal(&gTimerCond);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerEnsureThreadLocked(void) {
|
||||||
|
pthread_condattr_t attr;
|
||||||
|
|
||||||
|
// Caller holds gTimerMutex. Start the single background thread lazily, on the first timer.
|
||||||
|
if (gThreadStarted) {
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
if (pthread_condattr_init(&attr) != 0) {
|
||||||
|
return calogErrUnsupportedE;
|
||||||
|
}
|
||||||
|
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
|
||||||
|
if (pthread_cond_init(&gTimerCond, &attr) != 0) {
|
||||||
|
pthread_condattr_destroy(&attr);
|
||||||
|
return calogErrUnsupportedE;
|
||||||
|
}
|
||||||
|
pthread_condattr_destroy(&attr);
|
||||||
|
gShutdown = false;
|
||||||
|
if (pthread_create(&gThread, NULL, timerThreadMain, NULL) != 0) {
|
||||||
|
pthread_cond_destroy(&gTimerCond);
|
||||||
|
return calogErrUnsupportedE;
|
||||||
|
}
|
||||||
|
gThreadStarted = true;
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerEveryNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)userData;
|
||||||
|
return timerSchedule(args, argCount, result, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerFindByIdLocked(int64_t id) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
for (index = 0; index < gCount; index++) {
|
||||||
|
if (gTimers[index].id == id) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t timerNowNs(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (int64_t)ts.tv_sec * 1000000000 + (int64_t)ts.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void timerPruneLocked(void) {
|
||||||
|
int32_t index;
|
||||||
|
|
||||||
|
index = 0;
|
||||||
|
while (index < gCount) {
|
||||||
|
if (gTimers[index].cancelled) {
|
||||||
|
calogFnRelease(gTimers[index].callback);
|
||||||
|
gTimers[index] = gTimers[gCount - 1];
|
||||||
|
gCount--;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t timerSchedule(CalogValueT *args, int32_t argCount, CalogValueT *result, bool periodic) {
|
||||||
|
const char *label;
|
||||||
|
double ms;
|
||||||
|
int64_t delayNs;
|
||||||
|
int64_t id;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
label = periodic ? "timerEvery expects (ms, function)" : "timerAfter expects (ms, function)";
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || args[1].type != calogFnE) {
|
||||||
|
return calogFail(result, calogErrArgE, label);
|
||||||
|
}
|
||||||
|
ms = (args[0].type == calogIntE) ? (double)args[0].as.i : args[0].as.r;
|
||||||
|
// Bound ms so ms*1e6 stays well within int64 nanoseconds. This also rejects +Inf and NaN
|
||||||
|
// (both fail the comparison); the cast below is otherwise undefined once the product
|
||||||
|
// exceeds INT64_MAX. 9.0e12 ms is ~285 years -- far beyond any real timer.
|
||||||
|
if (!(ms >= 0.0 && ms <= 9.0e12)) {
|
||||||
|
return calogFail(result, calogErrRangeE, "timer: milliseconds out of range");
|
||||||
|
}
|
||||||
|
delayNs = (int64_t)(ms * 1000000.0);
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
if (gCount == gCap) {
|
||||||
|
int32_t newCap;
|
||||||
|
TimerEntryT *grown;
|
||||||
|
newCap = (gCap == 0) ? TIMER_INITIAL_CAP : gCap * TIMER_GROWTH;
|
||||||
|
grown = (TimerEntryT *)realloc(gTimers, (size_t)newCap * sizeof(TimerEntryT));
|
||||||
|
if (grown == NULL) {
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
return calogFail(result, calogErrOomE, "timer: out of memory");
|
||||||
|
}
|
||||||
|
gTimers = grown;
|
||||||
|
gCap = newCap;
|
||||||
|
}
|
||||||
|
status = timerEnsureThreadLocked();
|
||||||
|
if (status != calogOkE) {
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
return calogFail(result, status, "timer: could not start the timer thread");
|
||||||
|
}
|
||||||
|
id = gNextId++;
|
||||||
|
// Retain the callback for as long as the list holds it; released on cancel/fire/shutdown.
|
||||||
|
calogFnRetain(args[1].as.fn);
|
||||||
|
gTimers[gCount].id = id;
|
||||||
|
gTimers[gCount].callback = args[1].as.fn;
|
||||||
|
gTimers[gCount].nextFireMonoNs = timerNowNs() + delayNs;
|
||||||
|
gTimers[gCount].intervalNs = periodic ? ((delayNs > 0) ? delayNs : 1) : 0;
|
||||||
|
gTimers[gCount].cancelled = false;
|
||||||
|
gCount++;
|
||||||
|
pthread_cond_signal(&gTimerCond);
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
calogValueInt(result, id);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void *timerThreadMain(void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
for (;;) {
|
||||||
|
int64_t nowNs;
|
||||||
|
int32_t index;
|
||||||
|
int32_t minIndex;
|
||||||
|
|
||||||
|
// Drop tombstoned timers first, so the scan below only sees live entries.
|
||||||
|
timerPruneLocked();
|
||||||
|
if (gShutdown) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (gCount == 0) {
|
||||||
|
pthread_cond_wait(&gTimerCond, &gTimerMutex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nowNs = timerNowNs();
|
||||||
|
minIndex = 0;
|
||||||
|
for (index = 1; index < gCount; index++) {
|
||||||
|
if (gTimers[index].nextFireMonoNs < gTimers[minIndex].nextFireMonoNs) {
|
||||||
|
minIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gTimers[minIndex].nextFireMonoNs > nowNs) {
|
||||||
|
struct timespec target;
|
||||||
|
int64_t fireAt;
|
||||||
|
fireAt = gTimers[minIndex].nextFireMonoNs;
|
||||||
|
target.tv_sec = (time_t)(fireAt / 1000000000);
|
||||||
|
target.tv_nsec = (long)(fireAt % 1000000000);
|
||||||
|
pthread_cond_timedwait(&gTimerCond, &gTimerMutex, &target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
CalogFnT *cb;
|
||||||
|
CalogValueT res;
|
||||||
|
int64_t id;
|
||||||
|
int32_t status;
|
||||||
|
bool oneShot;
|
||||||
|
|
||||||
|
cb = gTimers[minIndex].callback;
|
||||||
|
id = gTimers[minIndex].id;
|
||||||
|
oneShot = (gTimers[minIndex].intervalNs == 0);
|
||||||
|
// Retain across the unlocked invoke so a concurrent cancel/shutdown can't free it.
|
||||||
|
calogFnRetain(cb);
|
||||||
|
if (oneShot) {
|
||||||
|
gTimers[minIndex].cancelled = true;
|
||||||
|
} else {
|
||||||
|
gTimers[minIndex].nextFireMonoNs += gTimers[minIndex].intervalNs;
|
||||||
|
if (gTimers[minIndex].nextFireMonoNs <= nowNs) {
|
||||||
|
// Fell behind (a slow callback): resync rather than fire in a burst.
|
||||||
|
gTimers[minIndex].nextFireMonoNs = nowNs + gTimers[minIndex].intervalNs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
|
||||||
|
status = calogFnInvoke(cb, NULL, 0, &res);
|
||||||
|
calogValueFree(&res);
|
||||||
|
if (status == calogErrDeadE) {
|
||||||
|
// The owning context is gone: auto-cancel the timer (find it again by id, since
|
||||||
|
// the list may have changed while unlocked).
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
index = timerFindByIdLocked(id);
|
||||||
|
if (index >= 0) {
|
||||||
|
gTimers[index].cancelled = true;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
}
|
||||||
|
calogFnRelease(cb);
|
||||||
|
pthread_mutex_lock(&gTimerMutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&gTimerMutex);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
29
libs/calogTimer.h
Normal file
29
libs/calogTimer.h
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// calogTimer.h -- calog timer library.
|
||||||
|
//
|
||||||
|
// One background thread drives every timer and invokes each script callback on its OWN
|
||||||
|
// owning context (marshalled like any calog function value, so the callback runs on the
|
||||||
|
// thread that created the timer). Three natives:
|
||||||
|
// timerAfter(ms, function) -> id fire the callback once, ms milliseconds from now
|
||||||
|
// timerEvery(ms, function) -> id fire the callback every ms milliseconds
|
||||||
|
// timerCancel(id) -> nil stop a pending or repeating timer (id from the above)
|
||||||
|
//
|
||||||
|
// If a timer's owning context is gone by the time it fires, the invoke fails cleanly and the
|
||||||
|
// timer auto-cancels itself. ms may be an integer or a real and must be non-negative. The
|
||||||
|
// registry (and its background thread) is process-wide and reference-counted across runtimes.
|
||||||
|
|
||||||
|
#ifndef CALOG_TIMER_H
|
||||||
|
#define CALOG_TIMER_H
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
|
||||||
|
// Register the timer natives (timerAfter, timerEvery, timerCancel) on a runtime. The shared
|
||||||
|
// registry is reference-counted, so this is safe to call on any number of runtimes.
|
||||||
|
int32_t calogTimerRegister(CalogT *calog);
|
||||||
|
|
||||||
|
// Release every still-pending timer's callback and (once the last registered runtime
|
||||||
|
// unregisters) stop the background thread and free the timer list. Like calogExport, call
|
||||||
|
// this while the timer-owning contexts are still ALIVE -- before you close them and before
|
||||||
|
// calogDestroy -- because a pending callback is a live reference into its owner's interpreter.
|
||||||
|
void calogTimerShutdown(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
165
tests/testCrypto.c
Normal file
165
tests/testCrypto.c
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// testCrypto.c -- exercises the crypto library: SHA-256/SHA-1 hashes, HMAC-SHA-256, base64
|
||||||
|
// and hex codecs (with round-trips and binary safety), random bytes, and UUIDs, driven from
|
||||||
|
// a Lua context.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogCrypto.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 16
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogCryptoRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx,
|
||||||
|
"report(1, #hashSha256('abc'))\n" // 64 hex chars
|
||||||
|
"report(2, hashSha256('abc') == 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' and 1 or 0)\n" // known vector
|
||||||
|
"report(3, hashSha1('abc') == 'a9993e364706816aba3e25717850c26c9cd0d89d' and 1 or 0)\n" // known vector
|
||||||
|
"report(4, hmacSha256('key', 'The quick brown fox jumps over the lazy dog') == 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8' and 1 or 0)\n" // known vector
|
||||||
|
"report(5, base64Encode('abc') == 'YWJj' and 1 or 0)\n" // known encoding
|
||||||
|
"report(6, base64Decode(base64Encode('hello')) == 'hello' and 1 or 0)\n" // round-trip, one pad byte
|
||||||
|
"report(7, hexEncode('abc') == '616263' and 1 or 0)\n" // known encoding
|
||||||
|
"report(8, hexDecode(hexEncode('Hello!')) == 'Hello!' and 1 or 0)\n" // round-trip
|
||||||
|
"report(9, #randomBytes(16))\n" // 16 bytes
|
||||||
|
"local a = uuid()\n"
|
||||||
|
"local b = uuid()\n"
|
||||||
|
"report(10, (#a == 36 and #b == 36 and a ~= b) and 1 or 0)\n" // distinct, well-formed
|
||||||
|
"report(11, base64Decode(base64Encode('a')) == 'a' and 1 or 0)\n" // round-trip, two pad bytes
|
||||||
|
"report(12, hexEncode('a\\0b') == '610062' and 1 or 0)\n" // binary-safe (embedded NUL)
|
||||||
|
"local ok = pcall(function() hexDecode('xyz') end)\n"
|
||||||
|
"report(13, ok and 0 or 1)\n" // invalid hex is catchable
|
||||||
|
"report(14, base64Decode('YQ==\\n') == 'a' and 1 or 0)\n" // trailing whitespace does not add spurious NULs
|
||||||
|
"done()");
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 64, "sha256 hex digest is 64 chars");
|
||||||
|
CHECK(atomic_load(&results[2]) == 1, "sha256('abc') matches the known vector");
|
||||||
|
CHECK(atomic_load(&results[3]) == 1, "sha1('abc') matches the known vector");
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "hmacSha256 matches the known vector");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "base64Encode('abc') is YWJj");
|
||||||
|
CHECK(atomic_load(&results[6]) == 1, "base64 round-trips a padded value");
|
||||||
|
CHECK(atomic_load(&results[7]) == 1, "hexEncode('abc') is 616263");
|
||||||
|
CHECK(atomic_load(&results[8]) == 1, "hex round-trips a value");
|
||||||
|
CHECK(atomic_load(&results[9]) == 16, "randomBytes(16) yields 16 bytes");
|
||||||
|
CHECK(atomic_load(&results[10]) == 1, "two uuids are distinct and 36 chars");
|
||||||
|
CHECK(atomic_load(&results[11]) == 1, "base64 round-trips a double-padded value");
|
||||||
|
CHECK(atomic_load(&results[12]) == 1, "hexEncode is binary-safe over embedded NULs");
|
||||||
|
CHECK(atomic_load(&results[13]) == 1, "invalid hex raises a catchable error");
|
||||||
|
CHECK(atomic_load(&results[14]) == 1, "base64Decode trims trailing whitespace without spurious trailing NULs");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
178
tests/testFs.c
Normal file
178
tests/testFs.c
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
// testFs.c -- exercises the filesystem library: write/read round-trip (binary-safe), append,
|
||||||
|
// exists transitions, stat (size + file/dir kind), directory listing, remove, and a catchable
|
||||||
|
// error, all driven from a Lua context in a private temp directory.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogFs.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
char script[4096];
|
||||||
|
char dir[64];
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogFsRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A private per-process temp dir keeps the test hermetic and re-runnable.
|
||||||
|
snprintf(dir, sizeof(dir), "/tmp/calogFsTest_%ld", (long)getpid());
|
||||||
|
snprintf(script, sizeof(script),
|
||||||
|
"local dir = '%s'\n"
|
||||||
|
"fsMkdir(dir)\n"
|
||||||
|
"local path = dir .. '/data.bin'\n"
|
||||||
|
"local data = 'hello\\0world'\n" // 11 bytes, embedded NUL
|
||||||
|
"fsWrite(path, data)\n"
|
||||||
|
"report(1, fsExists(path) and 1 or 0)\n" // 1, the file now exists
|
||||||
|
"local back = fsRead(path)\n"
|
||||||
|
"report(2, back == data and 1 or 0)\n" // 1, binary-safe round-trip
|
||||||
|
"report(3, #back)\n" // 11, whole file read by size
|
||||||
|
"local st = fsStat(path)\n"
|
||||||
|
"report(4, st.size)\n" // 11, stat size
|
||||||
|
"report(5, (st.isFile and not st.isDir) and 1 or 0)\n" // 1, it is a regular file
|
||||||
|
"fsAppend(path, '!')\n"
|
||||||
|
"report(6, #fsRead(path))\n" // 12, append added a byte
|
||||||
|
"local found = 0\n"
|
||||||
|
"for _, name in ipairs(fsList(dir)) do\n"
|
||||||
|
" if name == 'data.bin' then found = 1 end\n"
|
||||||
|
"end\n"
|
||||||
|
"report(7, found)\n" // 1, listing sees the file
|
||||||
|
"report(8, (fsStat(dir).isDir and not fsStat(dir).isFile) and 1 or 0)\n" // 1, directory stat
|
||||||
|
"local ok = pcall(function() fsRead(dir .. '/nope.dat') end)\n"
|
||||||
|
"report(9, ok and 0 or 1)\n" // 1, missing read is catchable
|
||||||
|
"fsRemove(path)\n"
|
||||||
|
"report(10, fsExists(path) and 0 or 1)\n" // 1, exists went false
|
||||||
|
"report(11, fsStat(path) == nil and 1 or 0)\n" // 1, stat of a missing path is nil
|
||||||
|
"fsRemove(dir)\n"
|
||||||
|
"done()", dir);
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx, script);
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 1, "fsWrite then fsExists sees the new file");
|
||||||
|
CHECK(atomic_load(&results[2]) == 1, "fsRead round-trips binary data with an embedded NUL");
|
||||||
|
CHECK(atomic_load(&results[3]) == 11, "fsRead returns the whole file by byte length");
|
||||||
|
CHECK(atomic_load(&results[4]) == 11, "fsStat.size matches the bytes written");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "fsStat reports a regular file (isFile, not isDir)");
|
||||||
|
CHECK(atomic_load(&results[6]) == 12, "fsAppend grew the file by one byte");
|
||||||
|
CHECK(atomic_load(&results[7]) == 1, "fsList contains the written file");
|
||||||
|
CHECK(atomic_load(&results[8]) == 1, "fsStat reports a directory (isDir, not isFile)");
|
||||||
|
CHECK(atomic_load(&results[9]) == 1, "reading a missing file raises a catchable error");
|
||||||
|
CHECK(atomic_load(&results[10]) == 1, "fsRemove deletes the file (fsExists transitions to false)");
|
||||||
|
CHECK(atomic_load(&results[11]) == 1, "fsStat of a missing path returns nil");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
233
tests/testHttp.c
Normal file
233
tests/testHttp.c
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
// testHttp.c -- exercises the HTTP client library. A tiny in-test HTTP/1.1 server runs on its
|
||||||
|
// own thread (bind an ephemeral 127.0.0.1 port, accept one connection, reply with a fixed
|
||||||
|
// Content-Length response), and a Lua context httpGet()s it and reports the status, body, and a
|
||||||
|
// lowercased header. Also covers the connect-refused and bad-scheme error paths. Kept to
|
||||||
|
// plain http:// so no TLS server is needed, though the binary still links OpenSSL because the
|
||||||
|
// library references it.
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogHttp.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
static int gListenFd = -1;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
static void *serverThread(void *arg);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void *serverThread(void *arg) {
|
||||||
|
static const char ok[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
|
||||||
|
// A hostile chunked response: a chunk size that saturates size_t. A correct client rejects
|
||||||
|
// it (a catchable error); a broken one loops forever in the dechunker.
|
||||||
|
static const char evil[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nffffffffffffffff\r\nx\r\n0\r\n\r\n";
|
||||||
|
char buf[2048];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
(void)arg;
|
||||||
|
// Serve two connections in order: the well-formed response, then the hostile one. Each
|
||||||
|
// consumes (part of) the request, replies, and closes so the client's read-to-EOF ends.
|
||||||
|
for (n = 0; n < 2; n++) {
|
||||||
|
int cfd;
|
||||||
|
cfd = accept(gListenFd, NULL, NULL);
|
||||||
|
if (cfd < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ssize_t received;
|
||||||
|
const char *response;
|
||||||
|
size_t length;
|
||||||
|
received = recv(cfd, buf, sizeof(buf), 0);
|
||||||
|
(void)received;
|
||||||
|
response = (n == 0) ? ok : evil;
|
||||||
|
length = (n == 0) ? (sizeof(ok) - 1) : (sizeof(evil) - 1);
|
||||||
|
(void)send(cfd, response, length, MSG_NOSIGNAL);
|
||||||
|
close(cfd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
pthread_t server;
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
socklen_t addrLen;
|
||||||
|
char script[512];
|
||||||
|
int port;
|
||||||
|
int yes;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogHttpRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-test HTTP/1.1 server on an ephemeral loopback port.
|
||||||
|
yes = 1;
|
||||||
|
gListenFd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (gListenFd < 0) {
|
||||||
|
printf("socket failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
setsockopt(gListenFd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
addr.sin_port = 0;
|
||||||
|
if (bind(gListenFd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||||
|
printf("bind failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (listen(gListenFd, 1) != 0) {
|
||||||
|
printf("listen failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
addrLen = sizeof(addr);
|
||||||
|
if (getsockname(gListenFd, (struct sockaddr *)&addr, &addrLen) != 0) {
|
||||||
|
printf("getsockname failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
port = (int)ntohs(addr.sin_port);
|
||||||
|
pthread_create(&server, NULL, serverThread, NULL);
|
||||||
|
|
||||||
|
snprintf(script, sizeof(script),
|
||||||
|
"local r = httpGet('http://127.0.0.1:%d/')\n"
|
||||||
|
"report(1, r.status)\n"
|
||||||
|
"report(2, r.body == 'hello' and 1 or 0)\n"
|
||||||
|
"report(3, r.headers['content-length'] == '5' and 1 or 0)\n"
|
||||||
|
"local okc = pcall(function() return httpGet('http://127.0.0.1:%d/') end)\n"
|
||||||
|
"report(6, okc and 0 or 1)\n" // hostile chunk size -> catchable error, not a hang
|
||||||
|
"local ok = pcall(function() httpGet('http://127.0.0.1:1/') end)\n"
|
||||||
|
"report(4, ok and 0 or 1)\n"
|
||||||
|
"local ok2 = pcall(function() httpGet('ftp://example/') end)\n"
|
||||||
|
"report(5, ok2 and 0 or 1)\n"
|
||||||
|
"done()", port, port);
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx, script);
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
pthread_join(server, NULL);
|
||||||
|
close(gListenFd);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 200, "httpGet returns status 200 from the in-test server");
|
||||||
|
CHECK(atomic_load(&results[2]) == 1, "httpGet returns the response body verbatim");
|
||||||
|
CHECK(atomic_load(&results[3]) == 1, "response headers are keyed by lowercased name");
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "a refused connection raises a catchable error");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "an unsupported URL scheme raises a catchable error");
|
||||||
|
CHECK(atomic_load(&results[6]) == 1, "a hostile oversized chunk size is rejected cleanly, not hung");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
157
tests/testJson.c
Normal file
157
tests/testJson.c
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
// testJson.c -- exercises the JSON library: parse, stringify, round-trip, types, escapes,
|
||||||
|
// nesting, and error handling, driven from a Lua context.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogJson.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogJsonRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx,
|
||||||
|
"local o = jsonParse('{\"a\":1,\"b\":2,\"nested\":{\"c\":3}}')\n"
|
||||||
|
"report(1, o.a + o.b + o.nested.c)\n" // 6, nested object
|
||||||
|
"local arr = jsonParse('[10,20,30]')\n"
|
||||||
|
"report(2, arr[1] + arr[2] + arr[3])\n" // 60, array
|
||||||
|
"local o2 = jsonParse(jsonStringify({x=5, y=7}))\n"
|
||||||
|
"report(3, o2.x + o2.y)\n" // 12, round-trip a map
|
||||||
|
"local back = jsonParse(jsonStringify('a\\nb\"c'))\n"
|
||||||
|
"report(4, back == 'a\\nb\"c' and 1 or 0)\n" // 1, escape round-trip
|
||||||
|
"report(5, jsonParse('3.5') * 2)\n" // 7, real
|
||||||
|
"report(6, (jsonParse('true') and jsonParse('null') == nil) and 1 or 0)\n" // 1
|
||||||
|
"local ok = pcall(function() jsonParse('{not valid}') end)\n"
|
||||||
|
"report(7, ok and 0 or 1)\n" // 1, parse error is catchable
|
||||||
|
"local oks = pcall(function() jsonParse([[\"\\uDC00\"]]) end)\n"
|
||||||
|
"report(8, oks and 0 or 1)\n" // 1, a lone low surrogate is rejected
|
||||||
|
"report(9, math.type(jsonParse('99999999999999999999')) == 'float' and 1 or 0)\n" // 1, an int that overflows int64 becomes a real
|
||||||
|
"done()");
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 6, "parse a nested object");
|
||||||
|
CHECK(atomic_load(&results[2]) == 60, "parse an array");
|
||||||
|
CHECK(atomic_load(&results[3]) == 12, "round-trip a map through stringify + parse");
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "strings with escapes round-trip");
|
||||||
|
CHECK(atomic_load(&results[5]) == 7, "parse a real number");
|
||||||
|
CHECK(atomic_load(&results[6]) == 1, "parse true and null");
|
||||||
|
CHECK(atomic_load(&results[7]) == 1, "invalid JSON raises a catchable error");
|
||||||
|
CHECK(atomic_load(&results[8]) == 1, "an unpaired low surrogate is rejected");
|
||||||
|
CHECK(atomic_load(&results[9]) == 1, "an integer literal overflowing int64 parses as a real");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
170
tests/testKv.c
Normal file
170
tests/testKv.c
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
// testKv.c -- exercises the kv library: set/get scalars and tables, deep-copy independence,
|
||||||
|
// has/delete, key listing, and rejection of function values, driven from a Lua context.
|
||||||
|
// calogKvShutdown() runs BEFORE calogDestroy (like testExport).
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogKv.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogKvRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx,
|
||||||
|
"kvSet('n', 42)\n"
|
||||||
|
"report(1, kvGet('n'))\n" // 42, store + fetch a scalar
|
||||||
|
"kvSet('x', 1)\n"
|
||||||
|
"local had = kvHas('x')\n"
|
||||||
|
"kvDelete('x')\n"
|
||||||
|
"report(2, (had and not kvHas('x')) and 1 or 0)\n" // 1, present then gone
|
||||||
|
"kvSet('t', {a=10, b=20})\n"
|
||||||
|
"local t = kvGet('t')\n"
|
||||||
|
"report(3, t.a + t.b)\n" // 30, store + fetch a table
|
||||||
|
"t.a = 999\n"
|
||||||
|
"local t2 = kvGet('t')\n"
|
||||||
|
"report(4, t2.a)\n" // 10, get returns an independent copy
|
||||||
|
"report(5, kvGet('missing') == nil and 1 or 0)\n" // 1, missing key -> nil
|
||||||
|
"local ok = pcall(function() kvSet('fn', function() return 1 end) end)\n"
|
||||||
|
"report(6, ok and 0 or 1)\n" // 1, function value is rejected
|
||||||
|
"report(7, kvHas('fn') and 0 or 1)\n" // 1, nothing was stored under 'fn'
|
||||||
|
"local okn = pcall(function() kvSet('nested', {cb = function() return 1 end}) end)\n"
|
||||||
|
"report(10, (okn == false and kvHas('nested') == false) and 1 or 0)\n" // 1, a fn nested in a table is rejected too
|
||||||
|
"kvSet('k1', 1)\n"
|
||||||
|
"kvSet('k2', 2)\n"
|
||||||
|
"kvSet('k3', 3)\n"
|
||||||
|
"report(8, #kvKeys())\n" // 5, keys: n, t, k1, k2, k3
|
||||||
|
"local found = 0\n"
|
||||||
|
"for _, k in ipairs(kvKeys()) do if k == 'n' then found = 1 end end\n"
|
||||||
|
"report(9, found)\n" // 1, keys come back as strings
|
||||||
|
"done()");
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 42, "kvGet returns a stored scalar");
|
||||||
|
CHECK(atomic_load(&results[2]) == 1, "kvHas is false after kvDelete");
|
||||||
|
CHECK(atomic_load(&results[3]) == 30, "kvGet returns a stored table");
|
||||||
|
CHECK(atomic_load(&results[4]) == 10, "kvGet returns an independent deep copy");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "kvGet on a missing key returns nil");
|
||||||
|
CHECK(atomic_load(&results[6]) == 1, "kvSet rejects a function value");
|
||||||
|
CHECK(atomic_load(&results[7]) == 1, "a rejected function value is not stored");
|
||||||
|
CHECK(atomic_load(&results[8]) == 5, "kvKeys lists every stored key");
|
||||||
|
CHECK(atomic_load(&results[9]) == 1, "kvKeys returns keys as strings");
|
||||||
|
CHECK(atomic_load(&results[10]) == 1, "kvSet rejects a function nested inside an aggregate");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogKvShutdown();
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
201
tests/testPubsub.c
Normal file
201
tests/testPubsub.c
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
// testPubsub.c -- exercises the pubsub library. Two Lua contexts subscribe to topic "t"; a
|
||||||
|
// third Lua context publishes. publish reaches both subscribers (count 2); after one context
|
||||||
|
// unsubscribes, publish reaches only the other (count 1); a published table payload is
|
||||||
|
// deep-copied and read back by the surviving subscriber; publishing an unheard topic returns
|
||||||
|
// 0; and publishing after calogPubsubShutdown is safe (the static registry is empty).
|
||||||
|
//
|
||||||
|
// The publisher is deliberately a SEPARATE context that is not itself subscribed to "t":
|
||||||
|
// publish runs inline on the publisher's thread and calogFnInvoke marshals each delivery to
|
||||||
|
// the subscriber's own thread, so a publisher that was subscribed would block waiting for
|
||||||
|
// itself (synchronous delivery is cyclic-deadlock-prone -- see calogPubsub.h).
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogPubsub.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *subA;
|
||||||
|
CalogContextT *subB;
|
||||||
|
CalogContextT *pub;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
if (calogPubsubRegister(calog) != calogOkE) {
|
||||||
|
printf("calogPubsubRegister failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two independent Lua contexts each subscribe to "t". Each callback records the payload it
|
||||||
|
// received into its own result slot (unwrapping a table's .v field so report always gets a
|
||||||
|
// number). subA keeps its subscription id in a global so a later eval can unsubscribe it.
|
||||||
|
subA = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(subA,
|
||||||
|
"subAId = subscribe('t', function(m)\n"
|
||||||
|
" if type(m) == 'table' then report(1, m.v) else report(1, m) end\n"
|
||||||
|
"end)\n"
|
||||||
|
"done()");
|
||||||
|
subB = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(subB,
|
||||||
|
"subscribe('t', function(m)\n"
|
||||||
|
" if type(m) == 'table' then report(2, m.v) else report(2, m) end\n"
|
||||||
|
"end)\n"
|
||||||
|
"done()");
|
||||||
|
pumpUntilDone(2);
|
||||||
|
|
||||||
|
// A third context (NOT subscribed to "t") publishes a number: both subscribers run.
|
||||||
|
pub = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(pub, "report(3, publish('t', 7))\n done()");
|
||||||
|
pumpUntilDone(3);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[3]) == 2, "publish reached both subscribers (count 2)");
|
||||||
|
CHECK(atomic_load(&results[1]) == 7, "subscriber A received the published number");
|
||||||
|
CHECK(atomic_load(&results[2]) == 7, "subscriber B received the published number");
|
||||||
|
|
||||||
|
// Unsubscribe A, then publish again: only B is invoked, and A's slot stays untouched.
|
||||||
|
atomic_store(&results[1], -1);
|
||||||
|
atomic_store(&results[2], -1);
|
||||||
|
calogContextEval(subA, "unsubscribe(subAId)\n done()");
|
||||||
|
pumpUntilDone(4);
|
||||||
|
calogContextEval(pub, "report(4, publish('t', 9))\n done()");
|
||||||
|
pumpUntilDone(5);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "after unsubscribe only one subscriber is invoked (count 1)");
|
||||||
|
CHECK(atomic_load(&results[2]) == 9, "the surviving subscriber received the number");
|
||||||
|
CHECK(atomic_load(&results[1]) == -1, "the unsubscribed subscriber was not invoked");
|
||||||
|
|
||||||
|
// Publish a table payload: publish deep-copies it, so B reads m.v out of its own copy.
|
||||||
|
atomic_store(&results[2], -1);
|
||||||
|
calogContextEval(pub, "report(6, publish('t', {v = 42}))\n done()");
|
||||||
|
pumpUntilDone(6);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[6]) == 1, "publishing a table reached the surviving subscriber (count 1)");
|
||||||
|
CHECK(atomic_load(&results[2]) == 42, "the subscriber received the deep-copied table payload");
|
||||||
|
|
||||||
|
// A topic nobody subscribed to delivers to no one.
|
||||||
|
calogContextEval(pub, "report(7, publish('none', 5))\n done()");
|
||||||
|
pumpUntilDone(7);
|
||||||
|
CHECK(atomic_load(&results[7]) == 0, "publishing to a topic with no subscribers returns 0");
|
||||||
|
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no errors during pubsub");
|
||||||
|
|
||||||
|
// Release every subscribed callback while the contexts are still alive, then confirm a
|
||||||
|
// publish after shutdown is safe and simply finds no subscribers.
|
||||||
|
calogPubsubShutdown();
|
||||||
|
calogContextEval(pub, "report(11, publish('t', 1))\n done()");
|
||||||
|
pumpUntilDone(8);
|
||||||
|
CHECK(atomic_load(&results[11]) == 0, "publishing after shutdown is safe and finds no subscribers");
|
||||||
|
|
||||||
|
calogContextClose(subA);
|
||||||
|
calogContextClose(subB);
|
||||||
|
calogContextClose(pub);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
146
tests/testTime.c
Normal file
146
tests/testTime.c
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// testTime.c -- exercises the time library: wall-clock and monotonic clock reads plus a
|
||||||
|
// millisecond sleep, driven from a Lua context.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogTime.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctx;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
calogTimeRegister(calog);
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctx,
|
||||||
|
"report(1, (timeNow() > 1600000000) and 1 or 0)\n" // wall clock is past 2020
|
||||||
|
"report(2, (timeMonotonic() >= 0) and 1 or 0)\n" // monotonic clock is non-negative
|
||||||
|
"local a = timeMonotonic()\n"
|
||||||
|
"local slept = timeSleep(20)\n"
|
||||||
|
"local b = timeMonotonic()\n"
|
||||||
|
"report(3, (b - a >= 0.015) and 1 or 0)\n" // ~20ms elapsed across the sleep
|
||||||
|
"report(4, (slept == nil) and 1 or 0)\n" // sleep returns nil
|
||||||
|
"report(5, (timeSleep(5.0) == nil) and 1 or 0)\n" // ms may be a real
|
||||||
|
"done()");
|
||||||
|
pumpUntilDone(1);
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 1, "timeNow is past 2020");
|
||||||
|
CHECK(atomic_load(&results[2]) == 1, "timeMonotonic is non-negative");
|
||||||
|
CHECK(atomic_load(&results[3]) == 1, "monotonic clock advances at least ~15ms across a 20ms sleep");
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "timeSleep returns nil");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "timeSleep accepts a real millisecond count");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
calogContextClose(ctx);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
190
tests/testTimer.c
Normal file
190
tests/testTimer.c
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// testTimer.c -- exercises the timer library from Lua: a one-shot fires once after a delay, a
|
||||||
|
// cancelled one-shot never fires, and a repeating timer fires several times before cancelling
|
||||||
|
// itself. Each callback runs on its owning context and reports back through host natives, which
|
||||||
|
// the main thread services by pumping.
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "calog.h"
|
||||||
|
#include "calogTimer.h"
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
||||||
|
|
||||||
|
#define PUMP_LIMIT 4000
|
||||||
|
#define RESULT_SLOTS 12
|
||||||
|
|
||||||
|
static CalogT *calog = NULL;
|
||||||
|
static _Atomic int64_t results[RESULT_SLOTS];
|
||||||
|
static _Atomic int32_t doneCount = 0;
|
||||||
|
static _Atomic int32_t errorCount = 0;
|
||||||
|
static int32_t testsRun = 0;
|
||||||
|
static int32_t testsFailed = 0;
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value);
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
||||||
|
static void pumpSettle(void);
|
||||||
|
static void pumpUntilDone(int32_t target);
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t asInt(const CalogValueT *value) {
|
||||||
|
if (value->type == calogIntE) {
|
||||||
|
return value->as.i;
|
||||||
|
}
|
||||||
|
return (int64_t)value->as.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
||||||
|
testsRun++;
|
||||||
|
if (!condition) {
|
||||||
|
testsFailed++;
|
||||||
|
printf("FAIL %s:%d %s\n", file, line, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
(void)args;
|
||||||
|
(void)argCount;
|
||||||
|
(void)userData;
|
||||||
|
atomic_fetch_add(&doneCount, 1);
|
||||||
|
calogValueNil(result);
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
||||||
|
int64_t tag;
|
||||||
|
|
||||||
|
(void)userData;
|
||||||
|
calogValueNil(result);
|
||||||
|
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) {
|
||||||
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
||||||
|
}
|
||||||
|
tag = asInt(&args[0]);
|
||||||
|
if (tag < 0 || tag >= RESULT_SLOTS) {
|
||||||
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
||||||
|
}
|
||||||
|
atomic_store(&results[tag], asInt(&args[1]));
|
||||||
|
return calogOkE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
||||||
|
(void)contextId;
|
||||||
|
(void)userData;
|
||||||
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
||||||
|
atomic_fetch_add(&errorCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpSettle(void) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
// Keep pumping for ~60ms so a broken cancel or a stray extra fire has time to show up.
|
||||||
|
for (i = 0; i < 120; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pumpUntilDone(int32_t target) {
|
||||||
|
struct timespec ts = { 0, 500000 };
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
||||||
|
calogPump(calog);
|
||||||
|
if (atomic_load(&doneCount) >= target) {
|
||||||
|
calogPump(calog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
CalogContextT *ctxAfter;
|
||||||
|
CalogContextT *ctxCancel;
|
||||||
|
CalogContextT *ctxEvery;
|
||||||
|
int32_t i;
|
||||||
|
|
||||||
|
calog = calogCreate();
|
||||||
|
if (calog == NULL) {
|
||||||
|
printf("calog create failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
calogSetErrorHandler(calog, onError, NULL);
|
||||||
|
calogRegister(calog, "report", nativeReport, NULL);
|
||||||
|
calogRegister(calog, "done", nativeDone, NULL);
|
||||||
|
if (calogTimerRegister(calog) != calogOkE) {
|
||||||
|
printf("calogTimerRegister failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
||||||
|
atomic_store(&results[i], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A one-shot fires its callback once, ~10ms from now, on its owning context.
|
||||||
|
ctxAfter = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctxAfter,
|
||||||
|
"timerAfter(10, function() report(1, 42); done() end)");
|
||||||
|
|
||||||
|
// A one-shot cancelled immediately must NEVER fire (slot 3 stays -1). Slot 4 confirms the
|
||||||
|
// script itself ran to completion.
|
||||||
|
ctxCancel = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctxCancel,
|
||||||
|
"local h = timerAfter(20, function() report(3, 999) end)\n"
|
||||||
|
"timerCancel(h)\n"
|
||||||
|
"report(4, 1)\n"
|
||||||
|
"local okr = pcall(function() timerAfter(1/0, function() end) end)\n"
|
||||||
|
"report(5, okr and 0 or 1)\n" // 1, a non-finite delay is rejected, not UB
|
||||||
|
"done()");
|
||||||
|
|
||||||
|
// A repeating timer fires every ~5ms; after the third fire it cancels itself and reports the
|
||||||
|
// final count. Slot 2 must settle at exactly 3.
|
||||||
|
ctxEvery = calogContextOpen(calog, &calogLuaEngine);
|
||||||
|
calogContextEval(ctxEvery,
|
||||||
|
"local count = 0\n"
|
||||||
|
"local id\n"
|
||||||
|
"id = timerEvery(5, function()\n"
|
||||||
|
" count = count + 1\n"
|
||||||
|
" report(2, count)\n"
|
||||||
|
" if count >= 3 then timerCancel(id); done() end\n"
|
||||||
|
"end)");
|
||||||
|
|
||||||
|
pumpUntilDone(3);
|
||||||
|
pumpSettle();
|
||||||
|
|
||||||
|
CHECK(atomic_load(&results[1]) == 42, "timerAfter fires its callback once after the delay");
|
||||||
|
CHECK(atomic_load(&results[4]) == 1, "a script that cancels a pending one-shot still runs to completion");
|
||||||
|
CHECK(atomic_load(&results[3]) == -1, "timerCancel of a one-shot before it fires prevents the callback");
|
||||||
|
CHECK(atomic_load(&results[2]) == 3, "timerEvery fires repeatedly and timerCancel stops it after 3 fires");
|
||||||
|
CHECK(atomic_load(&results[5]) == 1, "a non-finite / out-of-range delay is rejected");
|
||||||
|
CHECK(atomic_load(&doneCount) >= 3, "all three timer scripts completed");
|
||||||
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
||||||
|
|
||||||
|
// Release pending callbacks + stop the thread while the contexts are still alive.
|
||||||
|
calogTimerShutdown();
|
||||||
|
|
||||||
|
calogContextClose(ctxAfter);
|
||||||
|
calogContextClose(ctxCancel);
|
||||||
|
calogContextClose(ctxEvery);
|
||||||
|
calogDestroy(calog);
|
||||||
|
|
||||||
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
||||||
|
fflush(stdout);
|
||||||
|
if (testsFailed != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue