diff --git a/src/glib/glib_ssl.c b/src/glib/glib_ssl.c index e7470aae8..6cfc7b92d 100644 --- a/src/glib/glib_ssl.c +++ b/src/glib/glib_ssl.c @@ -67,3 +67,83 @@ nats_initSSL(void) return NATS_UPDATE_ERR_STACK(s); } +static bool hashNoErrorOnNoSSL = false; + +void +nats_hashNoErrorOnNoSSL(bool noError) +{ + hashNoErrorOnNoSSL = noError; +} + +natsStatus +nats_hashNew(nats_hash **new_hash) +{ +#if defined(NATS_HAS_TLS) + EVP_MD_CTX *h = EVP_MD_CTX_new(); + if (h == NULL) + return nats_setError(NATS_SSL_ERROR, "unable to create hash: %s", NATS_SSL_ERR_REASON_STRING); + + if (!EVP_DigestInit_ex(h, EVP_sha256(), NULL)) + { + EVP_MD_CTX_free(h); + return nats_setError(NATS_SSL_ERROR, "unable to create hash: %s", NATS_SSL_ERR_REASON_STRING); + } + *new_hash = (nats_hash*) h; + return NATS_OK; +#else + if (hashNoErrorOnNoSSL) + { + *new_hash = NULL; + return NATS_OK; + } + return nats_setError(NATS_ILLEGAL_STATE, "%s", NO_SSL_ERR); +#endif +} + +natsStatus +nats_hashWrite(nats_hash *hash, const void *data, int dataLen) +{ +#if defined(NATS_HAS_TLS) + if (!EVP_DigestUpdate((EVP_MD_CTX*) hash, data, (size_t) dataLen)) + return nats_setError(NATS_SSL_ERROR, "error writing into hash: %s", NATS_SSL_ERR_REASON_STRING); + return NATS_OK; +#else + if (hashNoErrorOnNoSSL) + return NATS_OK; + return nats_setError(NATS_ILLEGAL_STATE, "%s", NO_SSL_ERR); +#endif +} + +natsStatus +nats_hashSum(nats_hash *hash, unsigned char *digest, unsigned int *len) +{ +#if defined(NATS_HAS_TLS) + if (!EVP_DigestFinal_ex((EVP_MD_CTX*) hash, digest, len)) + return nats_setError(NATS_SSL_ERROR, "error finalizing hash: %s", NATS_SSL_ERR_REASON_STRING); + + return NATS_OK; + +#else + if (hashNoErrorOnNoSSL) + { + const char *nss = "not supported"; + const unsigned int slen = (unsigned int) strlen(nss); + memcpy(digest, nss, (size_t) slen); + *len = slen; + return NATS_OK; + } + return nats_setError(NATS_ILLEGAL_STATE, "%s", NO_SSL_ERR); +#endif +} + +void +nats_hashDestroy(nats_hash *hash) +{ + if (hash == NULL) + return; + +#if defined(NATS_HAS_TLS) + EVP_MD_CTX_free((EVP_MD_CTX*) hash); +#endif +} + diff --git a/src/js.c b/src/js.c index 949d1f3b6..482910ee5 100644 --- a/src/js.c +++ b/src/js.c @@ -80,7 +80,9 @@ _destroyOptions(jsOptions *o) static void _freeContext(jsCtx *js) { - natsConnection *nc = NULL; + natsConnection *nc = NULL; + void *arg = NULL; + js_onReleaseCb cb = NULL; natsStrHash_Destroy(js->pm); natsSubscription_Destroy(js->rsub); @@ -90,8 +92,13 @@ _freeContext(jsCtx *js) natsMutex_Destroy(js->mu); natsTimer_Destroy(js->pmtmr); nc = js->nc; + cb = js->onReleaseCb; + arg = js->onReleaseCbArg; NATS_FREE(js); + if (cb != NULL) + cb(arg); + natsConn_release(nc); } @@ -3651,3 +3658,12 @@ jsSub_checkOrderedMsg(natsSubscription *sub, natsMsg *msg, bool *reset) } return NATS_UPDATE_ERR_STACK(s); } + +void +js_setOnReleasedCb(jsCtx *js, js_onReleaseCb cb, void *arg) +{ + js_lock(js); + js->onReleaseCb = cb; + js->onReleaseCbArg = arg; + js_unlock(js); +} diff --git a/src/js.h b/src/js.h index 06f6ce920..be47607ec 100644 --- a/src/js.h +++ b/src/js.h @@ -24,7 +24,7 @@ void js_unlock(jsCtx *js); // We know what we are doing :-) #define js_lock(js) (natsMutex_Lock((js)->mu)) -#define js_unlock(c) (natsMutex_Unlock((js)->mu)) +#define js_unlock(js) (natsMutex_Unlock((js)->mu)) #endif // DEV_MODE @@ -291,3 +291,6 @@ js_checkFetchedMsg(natsSubscription *sub, natsMsg *msg, uint64_t fetchID, bool c natsStatus js_maybeFetchMore(natsSubscription *sub, jsFetch *fetch); + +void +js_setOnReleasedCb(jsCtx *js, js_onReleaseCb cb, void *arg); diff --git a/src/jsm.c b/src/jsm.c index d3f845db3..53f527d9d 100644 --- a/src/jsm.c +++ b/src/jsm.c @@ -32,28 +32,6 @@ typedef struct apiPaged } apiPaged; -static natsStatus -_marshalTimeUTC(natsBuffer *buf, bool sep, const char *fieldName, int64_t timeUTC) -{ - natsStatus s = NATS_OK; - char dbuf[36] = {'\0'}; - - s = nats_EncodeTimeUTC(dbuf, sizeof(dbuf), timeUTC); - if (s != NATS_OK) - return nats_setError(NATS_ERR, "unable to encode data for field '%s' value %" PRId64, fieldName, timeUTC); - - if (sep) - s = natsBuf_AppendByte(buf, ','); - - IFOK(s, natsBuf_AppendByte(buf, '"')); - IFOK(s, natsBuf_Append(buf, fieldName, -1)); - IFOK(s, natsBuf_Append(buf, "\":\"", -1)); - IFOK(s, natsBuf_Append(buf, dbuf, -1)); - IFOK(s, natsBuf_AppendByte(buf, '"')); - - return NATS_UPDATE_ERR_STACK(s); -} - // // Stream related functions // @@ -356,7 +334,7 @@ _marshalStreamSource(jsStreamSource *source, const char *fieldName, natsBuffer * if ((s == NATS_OK) && (source->OptStartSeq > 0)) s = nats_marshalLong(buf, true, "opt_start_seq", source->OptStartSeq); if ((s == NATS_OK) && (source->OptStartTime > 0)) - IFOK(s, _marshalTimeUTC(buf, true, "opt_start_time", source->OptStartTime)); + IFOK(s, nats_marshalTimeUTC(buf, true, "opt_start_time", source->OptStartTime)); if (source->FilterSubject != NULL) { IFOK(s, natsBuf_Append(buf, ",\"filter_subject\":\"", -1)); @@ -887,7 +865,7 @@ js_marshalStreamConfig(natsBuffer **new_buf, jsStreamConfig *cfg) if ((s == NATS_OK) && cfg->DiscardNewPerSubject) IFOK(s, natsBuf_Append(buf, ",\"discard_new_per_subject\":true", -1)); - IFOK(s, nats_marshalMetadata(buf, true, "metadata", cfg->Metadata)); + IFOK(s, nats_marshalMetadata(buf, true, "metadata", &(cfg->Metadata))); IFOK(s, _marshalStorageCompression(cfg->Compression, buf)); IFOK(s, nats_marshalULong(buf, true, "first_seq", cfg->FirstSeq)); IFOK(s, _marshalSubjectTransformConfig(&cfg->SubjectTransform, buf)); @@ -1779,14 +1757,34 @@ _purgeOrDelete(bool purge, jsCtx *js, const char *stream, jsOptions *opts, jsErr natsStatus js_PurgeStream(jsCtx *js, const char *stream, jsOptions *opts, jsErrCode *errCode) { - natsStatus s = _purgeOrDelete(true, js, stream, opts, errCode); + natsStatus s = NATS_OK; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); + + s = _purgeOrDelete(true, js, stream, opts, pErr); + // On "not found", clear the error stack and return the error. + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSStreamNotFoundErr)) + { + nats_clearLastError(); + return s; + } return NATS_UPDATE_ERR_STACK(s); } natsStatus js_DeleteStream(jsCtx *js, const char *stream, jsOptions *opts, jsErrCode *errCode) { - natsStatus s = _purgeOrDelete(false, js, stream, opts, errCode); + natsStatus s = NATS_OK; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); + + s = _purgeOrDelete(false, js, stream, opts, pErr); + // On "not found", clear the error stack and return the error. + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSStreamNotFoundErr)) + { + nats_clearLastError(); + return s; + } return NATS_UPDATE_ERR_STACK(s); } @@ -2927,7 +2925,7 @@ _marshalConsumerCreateReq(natsBuffer **new_buf, const char *stream, jsConsumerCo if ((s == NATS_OK) && (cfg->OptStartSeq > 0)) s = nats_marshalLong(buf, true, "opt_start_seq", cfg->OptStartSeq); if ((s == NATS_OK) && (cfg->OptStartTime > 0)) - s = _marshalTimeUTC(buf, true, "opt_start_time", cfg->OptStartTime); + s = nats_marshalTimeUTC(buf, true, "opt_start_time", cfg->OptStartTime); IFOK(s, _marshalAckPolicy(buf, cfg->AckPolicy)); if ((s == NATS_OK) && (cfg->AckWait > 0)) s = nats_marshalLong(buf, true, "ack_wait", cfg->AckWait); @@ -2941,9 +2939,9 @@ _marshalConsumerCreateReq(natsBuffer **new_buf, const char *stream, jsConsumerCo } if ((s == NATS_OK) && (cfg->FilterSubjectsLen > 0)) s = nats_marshalStringArray(buf, true, "filter_subjects", cfg->FilterSubjects, cfg->FilterSubjectsLen); - IFOK(s, nats_marshalMetadata(buf, true, "metadata", cfg->Metadata)); + IFOK(s, nats_marshalMetadata(buf, true, "metadata", &(cfg->Metadata))); if ((s == NATS_OK) && (cfg->PauseUntil > 0)) - s = _marshalTimeUTC(buf, true, "pause_until", cfg->PauseUntil); + s = nats_marshalTimeUTC(buf, true, "pause_until", cfg->PauseUntil); if ((s == NATS_OK) && !nats_IsStringEmpty(cfg->PriorityPolicy)) { s = natsBuf_Append(buf, ",\"priority_policy\":\"", -1); @@ -3390,6 +3388,8 @@ js_GetConsumerInfo(jsConsumerInfo **new_ci, jsCtx *js, bool freePfx = false; natsConnection *nc = NULL; natsMsg *resp = NULL; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); jsOptions o; if (errCode != NULL) @@ -3420,17 +3420,16 @@ js_GetConsumerInfo(jsConsumerInfo **new_ci, jsCtx *js, IFOK_JSR(s, natsConnection_Request(&resp, nc, subj, NULL, 0, o.Wait)); // If we got a response, check for error or return the consumer info result. - IFOK(s, _unmarshalConsumerCreateOrGetResp(new_ci, resp, errCode)); + IFOK(s, _unmarshalConsumerCreateOrGetResp(new_ci, resp, pErr)); NATS_FREE(subj); natsMsg_Destroy(resp); - if (s == NATS_NOT_FOUND) + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSConsumerNotFoundErr)) { nats_clearLastError(); return s; } - return NATS_UPDATE_ERR_STACK(s); } @@ -3444,6 +3443,8 @@ js_DeleteConsumer(jsCtx *js, const char *stream, const char *consumer, natsConnection *nc = NULL; natsMsg *resp = NULL; bool success = false; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); jsOptions o; if (errCode != NULL) @@ -3474,13 +3475,18 @@ js_DeleteConsumer(jsCtx *js, const char *stream, const char *consumer, IFOK_JSR(s, natsConnection_Request(&resp, nc, subj, NULL, 0, o.Wait)); // If we got a response, check for error and success result. - IFOK(s, _unmarshalSuccessResp(&success, resp, errCode)); + IFOK(s, _unmarshalSuccessResp(&success, resp, pErr)); if ((s == NATS_OK) && !success) s = nats_setError(s, "failed to delete consumer '%s'", consumer); NATS_FREE(subj); natsMsg_Destroy(resp); + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSConsumerNotFoundErr)) + { + nats_clearLastError(); + return s; + } return NATS_UPDATE_ERR_STACK(s); } @@ -3548,7 +3554,7 @@ _marshalConsumerPauseReq(natsBuffer **new_buf, uint64_t pauseUntil) s = natsBuf_Create(&buf, 256); IFOK(s, natsBuf_AppendByte(buf, '{')); if ((s == NATS_OK) && (pauseUntil > 0)) { - s = _marshalTimeUTC(buf, false, "pause_until", pauseUntil); + s = nats_marshalTimeUTC(buf, false, "pause_until", pauseUntil); } IFOK(s, natsBuf_AppendByte(buf, '}')); @@ -3575,13 +3581,15 @@ js_PauseConsumer(jsConsumerPauseResponse **new_cpr, jsCtx *js, const char *stream, const char *consumer, uint64_t pauseUntil, jsOptions *opts, jsErrCode *errCode) { - natsStatus s = NATS_OK; - char *subj = NULL; - bool freePfx = false; - natsConnection *nc = NULL; - natsBuffer *buf = NULL; - natsMsg *resp = NULL; - jsOptions o; + natsStatus s = NATS_OK; + char *subj = NULL; + bool freePfx = false; + natsConnection *nc = NULL; + natsBuffer *buf = NULL; + natsMsg *resp = NULL; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); + jsOptions o; if (errCode != NULL) *errCode = 0; @@ -3613,18 +3621,17 @@ js_PauseConsumer(jsConsumerPauseResponse **new_cpr, jsCtx *js, IFOK_JSR(s, natsConnection_Request(&resp, nc, subj, natsBuf_Data(buf), natsBuf_Len(buf), o.Wait)); // If we got a response, check for error or return the consumer info result. - IFOK(s, _unmarshalConsumerPauseResp(new_cpr, resp, errCode)); + IFOK(s, _unmarshalConsumerPauseResp(new_cpr, resp, pErr)); NATS_FREE(subj); natsMsg_Destroy(resp); natsBuf_Destroy(buf); - if (s == NATS_NOT_FOUND) + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSConsumerNotFoundErr)) { nats_clearLastError(); return s; } - return NATS_UPDATE_ERR_STACK(s); } @@ -3638,6 +3645,8 @@ js_UnpinConsumer(jsCtx *js, const char *stream, const char *consumer, const char natsConnection *nc = NULL; natsMsg *resp = NULL; bool success = false; + jsErrCode jsErr = 0; + jsErrCode *pErr = (errCode == NULL ? &jsErr : errCode); jsOptions o; char jsonBuf[64]; @@ -3675,13 +3684,18 @@ js_UnpinConsumer(jsCtx *js, const char *stream, const char *consumer, const char IFOK_JSR(s, natsConnection_RequestString(&resp, nc, subj, jsonBuf, o.Wait)); // If we got a response, check for error and success result. - IFOK(s, _unmarshalSuccessResp(&success, resp, errCode)); + IFOK(s, _unmarshalSuccessResp(&success, resp, pErr)); if ((s == NATS_OK) && !success) s = nats_setError(s, "failed to unpin group '%s' at consumer '%s'", group, consumer); NATS_FREE(subj); natsMsg_Destroy(resp); + if ((s == NATS_NOT_FOUND) && ((*pErr) == JSConsumerNotFoundErr)) + { + nats_clearLastError(); + return s; + } return NATS_UPDATE_ERR_STACK(s); } @@ -4121,7 +4135,7 @@ js_cloneConsumerConfig(jsConsumerConfig *org, jsConsumerConfig **clone) c->FilterSubjectsLen++; } } - IFOK(s, nats_cloneMetadata(&(c->Metadata), org->Metadata)); + IFOK(s, nats_cloneMetadata(&(c->Metadata), &(org->Metadata))); if (s == NATS_OK) *clone = c; else diff --git a/src/kv.c b/src/kv.c index 81f8e453e..b6383b323 100644 --- a/src/kv.c +++ b/src/kv.c @@ -94,24 +94,6 @@ kvConfig_Init(kvConfig *cfg) return NATS_OK; } -static bool -validBucketName(const char *bucket) -{ - int i; - char c; - - if (nats_IsStringEmpty(bucket)) - return false; - - for (i=0; i<(int)strlen(bucket); i++) - { - c = bucket[i]; - if ((isalnum((unsigned char) c) == 0) && (c != '_') && (c != '-')) - return false; - } - return true; -} - static void _freeKV(kvStore *kv) { @@ -166,7 +148,7 @@ _createKV(kvStore **new_kv, jsCtx *js, const char *bucket) natsStatus s = NATS_OK; kvStore *kv = NULL; - if (!validBucketName(bucket)) + if (!nats_validBucketName(bucket)) return nats_setError(NATS_INVALID_ARG, "%s", kvErrInvalidBucketName); kv = (kvStore*) NATS_CALLOC(1, sizeof(kvStore)); @@ -446,7 +428,7 @@ js_DeleteKeyValue(jsCtx *js, const char *bucket) if (js == NULL) return nats_setDefaultError(NATS_INVALID_ARG); - if (!validBucketName(bucket)) + if (!nats_validBucketName(bucket)) return nats_setError(NATS_INVALID_ARG, "%s", kvErrBadBucket); if (nats_asprintf(&stream, kvBucketNameTmpl, bucket) < 0) diff --git a/src/micro.c b/src/micro.c index 3bcb0dc81..f5afb3eef 100644 --- a/src/micro.c +++ b/src/micro.c @@ -561,7 +561,7 @@ _clone_service_config(microServiceConfig **out, microServiceConfig *cfg) MICRO_CALL(err, micro_strdup((char **)&new_cfg->Description, cfg->Description)); MICRO_CALL(err, micro_strdup((char **)&new_cfg->QueueGroup, cfg->QueueGroup)); MICRO_CALL(err, micro_ErrorFromStatus( - nats_cloneMetadata(&new_cfg->Metadata, cfg->Metadata))); + nats_cloneMetadata(&new_cfg->Metadata, &(cfg->Metadata)))); MICRO_CALL(err, micro_clone_endpoint_config(&new_cfg->Endpoint, cfg->Endpoint)); if (err != NULL) { @@ -802,7 +802,7 @@ microService_GetInfo(microServiceInfo **new_info, microService *m) MICRO_CALL(err, micro_strdup((char **)&info->Description, m->cfg->Description)); MICRO_CALL(err, micro_strdup((char **)&info->Id, m->id)); MICRO_CALL(err, micro_ErrorFromStatus( - nats_cloneMetadata(&info->Metadata, m->cfg->Metadata))); + nats_cloneMetadata(&info->Metadata, &(m->cfg->Metadata)))); if (err == NULL) { @@ -833,7 +833,7 @@ microService_GetInfo(microServiceInfo **new_info, microService *m) MICRO_CALL(err, micro_strdup((char **)&info->Endpoints[len].Subject, ep->subject)); MICRO_CALL(err, micro_strdup((char **)&info->Endpoints[len].QueueGroup, micro_queue_group_for_endpoint(ep))); MICRO_CALL(err, micro_ErrorFromStatus( - nats_cloneMetadata(&info->Endpoints[len].Metadata, ep->config->Metadata))); + nats_cloneMetadata(&info->Endpoints[len].Metadata, &(ep->config->Metadata)))); if (err == NULL) { len++; diff --git a/src/micro_endpoint.c b/src/micro_endpoint.c index 3acfbdbdf..4471198e4 100644 --- a/src/micro_endpoint.c +++ b/src/micro_endpoint.c @@ -298,7 +298,7 @@ micro_clone_endpoint_config(microEndpointConfig **out, microEndpointConfig *cfg) MICRO_CALL(err, micro_strdup((char **)&new_cfg->Subject, cfg->Subject)); MICRO_CALL(err, micro_strdup((char **)&new_cfg->QueueGroup, cfg->QueueGroup)); MICRO_CALL(err, micro_ErrorFromStatus( - nats_cloneMetadata(&new_cfg->Metadata, cfg->Metadata))); + nats_cloneMetadata(&new_cfg->Metadata, &(cfg->Metadata)))); if (err != NULL) { diff --git a/src/micro_monitoring.c b/src/micro_monitoring.c index 0113360ab..d6a56a5d5 100644 --- a/src/micro_monitoring.c +++ b/src/micro_monitoring.c @@ -266,7 +266,7 @@ marshal_info(natsBuffer **new_buf, microServiceInfo *info) { IFOK(s, natsBuf_AppendByte(buf, '{')); IFOK_attr("name", info->Endpoints[i].Name, ""); - IFOK(s, nats_marshalMetadata(buf, true, "metadata", info->Endpoints[i].Metadata)); + IFOK(s, nats_marshalMetadata(buf, true, "metadata", &(info->Endpoints[i].Metadata))); IFOK(s, natsBuf_AppendByte(buf, ',')); if (!nats_IsStringEmpty(info->Endpoints[i].QueueGroup)) IFOK_attr("queue_group", info->Endpoints[i].QueueGroup, ","); @@ -279,7 +279,7 @@ marshal_info(natsBuffer **new_buf, microServiceInfo *info) } IFOK_attr("id", info->Id, ""); - IFOK(s, nats_marshalMetadata(buf, true, "metadata", info->Metadata)); + IFOK(s, nats_marshalMetadata(buf, true, "metadata", &(info->Metadata))); IFOK(s, natsBuf_AppendByte(buf, ',')); IFOK_attr("name", info->Name, ","); IFOK_attr("type", info->Type, ","); diff --git a/src/msg.h b/src/msg.h index 3db290019..534541262 100644 --- a/src/msg.h +++ b/src/msg.h @@ -116,6 +116,12 @@ natsMsg_createWithPadding(natsMsg **newMsg, const char *buf, int bufLen, int bufPaddingSize, int hdrLen); +natsStatus +natsHeaderValue_create(natsHeaderValue **retV, const char *value, bool makeCopy); + +void +natsHeaderValue_free(natsHeaderValue *v, bool all); + void natsMsg_freeHeaders(natsMsg *msg); diff --git a/src/nats.c b/src/nats.c index fa592d36b..67281e892 100644 --- a/src/nats.c +++ b/src/nats.c @@ -20,6 +20,7 @@ #include "glib/glib.h" #include "sub.h" #include "conn.h" +#include "msg.h" #if defined(_WIN32) && _WIN32 #ifndef NATS_STATIC @@ -213,3 +214,229 @@ nats_SetMessageDeliveryPoolSize(int max) s = nats_setMessageDispatcherPoolCap(max); return NATS_UPDATE_ERR_STACK(s); } + +void +natsHeader_Destroy(natsHeader *nh) +{ + natsStrHashIter iter; + natsStrHash *h; + void *p = NULL; + + if (nh == NULL) + return; + + h = (natsStrHash*) nh; + natsStrHashIter_Init(&iter, h); + for (;natsStrHashIter_Next(&iter, NULL, &p);) + { + natsHeaderValue *v = (natsHeaderValue *)p; + natsHeaderValue_free(v, true); + } + natsStrHash_Destroy(h); +} + +natsStatus +natsHeader_New(natsHeader **new_header) +{ + natsStatus s; + + if (new_header == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + s = natsStrHash_Create((natsStrHash**) new_header, 4); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +natsHeader_Set(natsHeader *nh, const char *key, const char *value) +{ + natsStatus s = NATS_OK; + natsHeaderValue *v = NULL; + natsStrHash *h = NULL; + + // Value can be empty, but not NULL. + if ((nh == NULL) || nats_IsStringEmpty(key) || (value == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + h = (natsStrHash*) nh; + s = natsHeaderValue_create(&v, value, true); + if (s == NATS_OK) + { + void *p = NULL; + + s = natsStrHash_Set(h, (char*) key, true, (void*) v, &p); + if (s != NATS_OK) + natsHeaderValue_free(v, false); + else if (p != NULL) + { + natsHeaderValue *old = (natsHeaderValue*) p; + natsHeaderValue_free(old, true); + } + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +natsHeader_Add(natsHeader *nh, const char *key, const char *value) +{ + natsStatus s; + natsHeaderValue *v = NULL; + natsStrHash *h = NULL; + + // Value can be empty, but not NULL. + if ((nh == NULL) || nats_IsStringEmpty(key) || (value == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + h = (natsStrHash*) nh; + s = natsHeaderValue_create(&v, value, true); + if (s == NATS_OK) + { + natsHeaderValue *cur = natsStrHash_Get(h, (char*) key); + if (cur != NULL) + { + for (; cur->next != NULL; ) + cur = cur->next; + + cur->next = v; + } + else + s = natsStrHash_Set(h, (char*) key, true, (void*) v, NULL); + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +natsHeader_Get(natsHeader *nh, const char *key, const char **value) +{ + natsHeaderValue *v = NULL; + natsStrHash *h = NULL; + + if ((nh == NULL) || nats_IsStringEmpty(key) || (value == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + *value = NULL; + h = (natsStrHash*) nh; + + if (natsStrHash_Count(h) == 0) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + v = natsStrHash_Get(h, (char*) key); + if (v == NULL) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + *value = (const char*) v->value; + return NATS_OK; +} + +natsStatus +natsHeader_Values(natsHeader *nh, const char *key, const char* **values, int *count) +{ + natsStatus s = NATS_OK; + int c = 0; + natsHeaderValue *cur = NULL; + const char* *strs = NULL; + natsHeaderValue *v = NULL; + natsStrHash *h = NULL; + + if ((nh == NULL) || nats_IsStringEmpty(key) || (values == NULL) || (count == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + *values = NULL; + *count = 0; + h = (natsStrHash*) nh; + + if (natsStrHash_Count(h) == 0) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + v = natsStrHash_Get(h, (char*) key); + if (v == NULL) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + for (cur=v; cur != NULL; cur = cur->next) + c++; + + strs = NATS_CALLOC(c, sizeof(char*)); + if (strs == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + { + int i = 0; + + for (cur=v; cur != NULL; cur = cur->next) + strs[i++] = (const char*) cur->value; + + *values = strs; + *count = c; + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +natsHeader_Keys(natsHeader *nh, const char* **keys, int *count) +{ + natsStatus s = NATS_OK; + const char* *strs = NULL; + int c = 0; + natsStrHash *h = NULL; + + if ((nh == NULL) || (keys == NULL) || (count == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + *keys = NULL; + *count = 0; + h = (natsStrHash*) nh; + + if ((c = natsStrHash_Count(h)) == 0) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + strs = NATS_CALLOC(c, sizeof(char*)); + if (strs == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + { + natsStrHashIter iter; + char *hk = NULL; + int i; + + natsStrHashIter_Init(&iter, h); + for (i=0; natsStrHashIter_Next(&iter, &hk, NULL); i++) + { + strs[i] = (const char*) hk; + } + natsStrHashIter_Done(&iter); + + *keys = strs; + *count = c; + } + return NATS_UPDATE_ERR_STACK(s); +} + +int +natsHeader_KeysCount(natsHeader *nh) +{ + if (nh == NULL) + return 0; + + return natsStrHash_Count((natsStrHash*) nh); +} + +natsStatus +natsHeader_Delete(natsHeader *nh, const char *key) +{ + natsHeaderValue *v = NULL; + natsStrHash *h = NULL; + + if ((nh == NULL) || nats_IsStringEmpty(key)) + return nats_setDefaultError(NATS_INVALID_ARG); + + h = (natsStrHash*) nh; + if (natsStrHash_Count(h) == 0) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + v = natsStrHash_Remove(h, (char*) key); + if (v == NULL) + return NATS_NOT_FOUND; // normal error, so don't update error stack + + natsHeaderValue_free(v, true); + return NATS_OK; +} diff --git a/src/nats.h b/src/nats.h index e6b402b14..514c5661d 100644 --- a/src/nats.h +++ b/src/nats.h @@ -196,6 +196,11 @@ typedef struct __natsOptions natsOptions; */ typedef char natsInbox; +/** \brief Map with key being a string and value being an array of strings. + * + * This is represented as a map, with the key being a string and value being an array of strings. + */ +typedef struct __natsHeader natsHeader; /** \brief An initial configuration for NATS client. Provides control over the * threading model, and sets many default option values. @@ -250,15 +255,19 @@ typedef struct natsMsgList /** \brief A type to represent user-provided metadata, a list of k=v pairs. * - * Used in JetStream, microservice configuration. + * Used in JetStream, object store and microservice configurations. */ - typedef struct natsMetadata { - // User-provided metadata for the stream, encoded as an array of {"key", "value",...} - const char **List; - // Number of key/value pairs in Metadata, 1/2 of the length of the array. - int Count; + /** + * @brief User-provided metadata for the stream, encoded as an array of {"key", "value",...} + */ + const char **List; + /** + * @brief Number of key/value pairs in Metadata, 1/2 of the length of the array. + */ + int Count; + } natsMetadata; /** @@ -1084,7 +1093,7 @@ typedef struct jsConsumerInfo * * \note Once done, the list should be destroyed calling #jsConsumerInfoList_Destroy * - * @see jsStreamInfoList_Destroy + * @see jsConsumerInfoList_Destroy */ typedef struct jsConsumerInfoList { @@ -1593,6 +1602,423 @@ typedef struct kvKeysList } kvKeysList; +/** + * The Object Store object. + */ +typedef struct __objStore objStore; + +/** + * The Object Store object returned when initiating a "put" operation. + * This is needed when using the generic #objStore_Put function because the + * user needs to add data to the object as the data becomes available. + * + * @see objStore_Put + * + * Most users don't need that and will use the followin convenience functions: + * - #objStore_PutString + * - #objStore_PutBytes + * - #objStore_PutFile + */ +typedef struct __objStorePut objStorePut; + +/** + * The Object Store object returned by #objStore_Get operation. + * + * @see objStore_Get + */ +typedef struct __objStoreGet objStoreGet; + +/** + * The Object Store watcher object. + */ +typedef struct __objStoreWatcher objStoreWatcher; + +/** + * Object store configuration object. + * + * In order to create an object store, a configuration needs to be set. + * The typical usage would be to initialize all required objects on the stack + * and configure them, then pass the pointer to the configuration to + * #js_CreateObjectStore. + * + * \note The strings are applications owned and will not be freed by the library. + * + * @see objStoreConfig_Init + * + * \code{.unparsed} + * objStore *obs = NULL; + * objStoreConfig cfg; + * + * objStoreConfig_Init(&cfg); + * cfg.Bucket = "TEST"; + * cfg.Description = "Testing ObjectStore"; + * cfg.Storage = js_MemoryStorage; + * cfg.TTL = 10000; // 10 seconds + * cfg.MaxBytes = 1024*1024*1024; + * cfg.Metadata.List = (const char*[4]){"field1", "value1", "field2", "value2"}; + * cfg.Metadata.Count = 2; + * s = js_CreateObjectStore(&obs, js, &cfg); + * \endcode + */ +typedef struct objStoreConfig +{ + /** + * @brief Bucket is the name of the object store. + * + * Bucket name has to be unique and can only contain alphanumeric characters, + * dashes, and underscores. + */ + const char *Bucket; + + /** + * @brief Description is an optional description for the object store. + */ + const char *Description; + + /** + * @brief TTL is the maximum age (expressed in milliseconds) of objects in the store. + * + * If an object is not updated within this time, it will be removed from the store. + * By default, objects do not expire. + */ + int64_t TTL; + + /** + * @brief MaxBytes is the maximum size of the object store. + * + * If not specified, the default is -1 (unlimited). + */ + int64_t MaxBytes; + + /** + * @brief Storage is the type of storage to use for the object store. + * + * If not specified, the default is FileStorage. + */ + jsStorageType Storage; + + /** + * @brief Replicas is the number of replicas to keep for the object store in clustered jetstream. + * + * Defaults to 1, maximum is 5. + */ + int Replicas; + + /** + * @brief Placement is used to declare where the object store should be placed. + * + * Placement is used to declare where the object store should be placed via + * tags and/or an explicit cluster name. + */ + jsPlacement *Placement; + + /** + * @brief Compression enables the underlying stream compression. + * + * \note Compression is supported for nats-server 2.10.0+ + */ + bool Compression; + + // /** + // * @brief Bucket-specific metadata. + // * + // * \note Metadata requires nats-server v2.10.0+ + // */ + natsMetadata Metadata; + +} objStoreConfig; + +/** + * List of object store names returned by #js_ObjectStoreNames + * + * \note Once done, the list should be destroyed calling #objStoreNamesList_Destroy + * + * @see objStoreNamesList_Destroy + */ +typedef struct objStoreNamesList +{ + char **List; + int Count; + +} objStoreNamesList; + +/** + * The Object Store status object. + * + * This is run-time status about a bucket. + * + * \note User needs to call #objStoreStatus_Destroy when object is + * no longer needed to free memory, except if the user got the status + * from the list #objStoreStatusesList, in which case the statuses will + * be destroyed when calling #objStoreStatusesList_Destroy. + * + * @see objStoreStatus_Destroy + * @see objStoreStatusesList_Destroy + */ +typedef struct objStoreStatus +{ + /** + * @brief Bucket is the name of the object store. + */ + const char *Bucket; + + /** + * @brief Description is the description supplied when creating the bucket. + */ + const char *Description; + + /** + * @brief TTL indicates how long (in milliseconds) objects are kept in the bucket. + */ + int64_t TTL; + + /** + * @brief Storage indicates the underlying JetStream storage technology used tostore data. + */ + jsStorageType Storage; + + /** + * @brief Replicas indicates how many storage replicas are kept for the data in the bucket. + */ + int Replicas; + + /** + * @brief Sealed indicates the stream is sealed and cannot be modified in any way. + */ + bool Sealed; + + /** + * @brief Size is the combined size of all data in the bucket including metadata, in bytes. + */ + uint64_t Size; + + /** + * @brief BackingStore indicates what technology is used for storage of the bucket. + * + * Currently only JetStream is supported. + */ + const char *BackingStore; + + /** + * @brief Metadata is the user supplied metadata for the bucket. + */ + natsMetadata Metadata; + + /** + * @brief StreamInfo is the stream info retrieved to create the status. + */ + jsStreamInfo *StreamInfo; + + /** + * @brief IsCompressed indicates if the data is compressed on disk. + */ + bool IsCompressed; + +} objStoreStatus; + +/** + * List of object store statuses returned by #js_ObjectStoreStatuses. + * + * \note Once done, the list should be destroyed calling #objStoreStatusesList_Destroy + * + * @see objStoreStatusesList_Destroy + */ +typedef struct objStoreStatusesList +{ + objStoreStatus **List; + int Count; + +} objStoreStatusesList; + +/** + * This structure is used to embed links to other buckets and objects. + */ +typedef struct objStoreLink +{ + /** + * @brief Bucket is the name of the object store the link is pointing to. + */ + const char *Bucket; + + /** + * @brief Name can be used to link to a single object. + * + * If empty means this is a link to the whole store, like a directory. + */ + const char *Name; + +} objStoreLink; + +/** + * This structure is used to set additional options when creating an object. + */ +typedef struct objStoreMetaOptions +{ + /** + * @brief Link contains information about a link to another object or object store. + * + * It should not be set manually, but rather by using the #objStore_AddLink or #objStore_AddBucketLink methods. + */ + objStoreLink *Link; + + /** + * @brief ChunkSize is the maximum size of each chunk in bytes. + * + * If not specified, the default is 128k. + */ + uint32_t ChunkSize; + +} objStoreMetaOptions; + +/** + * This structure represents high level information about an object. + */ +typedef struct objStoreMeta +{ + /** + * @brief Name is the name of the object. + * + * The name is required when adding an object and has to be unique within + * the object store. + */ + const char *Name; + + /** + * @brief Description is an optional description for the object. + */ + const char *Description; + + /** + * @brief Headers is an optional set of user-defined headers for the object. + */ + natsHeader *Headers; + + /** + * @brief Metadata is the user supplied metadata for the object. + */ + natsMetadata Metadata; + + /** + * @brief Additional options for the object. + */ + objStoreMetaOptions Opts; + +} objStoreMeta; + +/** + * This structure contains #objStoreMeta and additional information about an object. + */ +typedef struct objStoreInfo +{ + /** + * @brief objStoreMeta contains high level information about the object. + */ + objStoreMeta Meta; + + /** + * @brief Bucket is the name of the object store. + */ + const char *Bucket; + + /** + * @brief NUID is the unique identifier for the object set when putting the object into the store. + */ + const char *NUID; + + /** + * @brief Size is the size of the object in bytes. It only includes the size of the object itself, not the metadata. + */ + uint64_t Size; + + /** + * @brief ModTime is the last modification time of the object (in unix nanoseconds). + */ + int64_t ModTime; + + /** + * @brief Chunks is the number of chunks the object is split into. + * + * Maximum size of each chunk can be specified in objStoreMetaOptions. + */ + uint32_t Chunks; + + /** + * @brief Digest is the SHA-256 digest of the object. + * + * It is used to verify the integrity of the object. + */ + const char *Digest; + + /** + * @brief Deleted indicates if the object is marked as deleted. + */ + bool Deleted; + +} objStoreInfo; + +/** + * List of object store information returned by #objStore_List. + * + * \note Once done, the list should be destroyed calling #objStoreInfoList_Destroy. + * + * @see objStoreInfoList_Destroy + */ +typedef struct objStoreInfoList +{ + objStoreInfo **List; + int Count; + +} objStoreInfoList; + +/** + * This structure is used to provide additional options to some object store APIs. + * + * Some options will have a meaning only for some APIs. Each API that accepts + * this structure will describe which options can be used and their exact meaning + * in the context of the API. + * + * Typically, users will declare a variable of this type on the stack and initializes + * it with #objStoreOptions_Init, and then pass the pointer to this variable to APIs + * that need it. + */ +typedef struct objStoreOptions +{ + /** + * @brief ShowDeleted includes deleted object in the result. + * + * For some APIs, if this option is set to true, the returned value will include + * objects that have been deleted. + */ + bool ShowDeleted; + +} objStoreOptions; + +/** + * The Object Store watcher options. + * + * Initialize the object with #objStoreWatchOptions_Init + */ +typedef struct objStoreWatchOptions +{ + /** + * @brief The watcher won't return information about deleted objects. + * + * If this is set to true, #objStoreWatcher_Next will not return information + * about deleted objects. + */ + bool IgnoreDeletes; + + /** + * @brief The watcher won't return information about existing objects. + * + * If this is set to true, #objStoreWatcher_Next will not return information + * about objects that have already been added to the object store. + */ + bool UpdatesOnly; + +} objStoreWatchOptions; + + #if defined(NATS_HAS_STREAMING) /** \brief A connection to a `NATS Streaming Server`. * @@ -3984,38 +4410,201 @@ natsInbox_Destroy(natsInbox *inbox); /** @} */ // end of inboxGroup -/** \defgroup msgGroup Message +/** \defgroup headerGroup Header * - * NATS Message. - * @{ + * This is represented as a map, with the key being a string and value being an array of strings. + * + * @{ */ -/** \brief Destroys this list of messages. +/** \brief Creates an header object. * - * This function iterates through the list of all messages and call #natsMsg_Destroy - * for each valid (not set to `NULL`) message. It then frees the array that was - * allocated to hold pointers to those messages. + * Creates an header object that is empty. * - * \note The #natsMsgList object itself is not freed since it is expected that - * users will pass a pointer to a stack object. Should the user create its own - * object, it will be the user responsibility to free this object. + * \note The returned #natsHeader object needs be destroyed using #natsHeader_Destroy + * when no longer needed to free allocated memory. * - * @param list the #natsMsgList list of #natsMsg objects to destroy. + * @see natsHeader_Destroy + * + * @param new_header the location where to store the pointer to the #natsHeader object. */ -NATS_EXTERN void -natsMsgList_Destroy(natsMsgList *list); +NATS_EXTERN natsStatus +natsHeader_New(natsHeader **new_header); -/** \brief Creates a #natsMsg object. - * - * Creates a #natsMsg object. This is used by the subscription related calls - * and by #natsConnection_PublishMsg(). + /** \brief Set the header entries associated with `key` to the single element `value`. * - * \note Messages need to be destroyed with #natsMsg_Destroy() when no - * longer needed. + * It will replace any existing value associated with `key`. * - * @see natsMsg_Destroy() + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. * - * @param newMsg the location where to store the pointer to the newly created + * @param h the pointer to the #natsHeader object. + * @param key the key under which the `value` will be stored. It can't ne `NULL` or empty. + * @param value the string to store under the given `key`. The value can be `NULL` or empty string. + */ +NATS_EXTERN natsStatus +natsHeader_Set(natsHeader *h, const char *key, const char *value); + +/** \brief Add `value` to the header associated with `key`. + * + * It will append to any existing values associated with `key`. + * + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. + * + * @param h the pointer to the #natsHeader object. + * @param key the key under which the `value` will be stored. It can't ne `NULL` or empty. + * @param value the string to add to the values associated with the given `key`. The value can be `NULL` or empty string. + */ +NATS_EXTERN natsStatus +natsHeader_Add(natsHeader *h, const char *key, const char *value); + +/** \brief Get the header entry associated with `key`. + * + * If more than one entry for the `key` is available, the first is returned. + * + * \warning The returned value is owned by the library and MUST not be freed or altered. + * + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. + * + * @param h the pointer to the #natsHeader object. + * @param key the key for which the value is requested. + * @param value the memory location where the library will store the pointer to the first + * value (if more than one is found) associated with the `key`. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. + */ +NATS_EXTERN natsStatus +natsHeader_Get(natsHeader *h, const char *key, const char **value); + +/** \brief Get all header values associated with `key`. + * + * \warning The returned strings are own by the library and MUST not be freed or altered. + * However, the returned array `values` MUST be freed by the user. + * + * \code{.c} + * const char* *values = NULL; + * int count = 0; + * + * s = natsHeader_Values(h, "My-Key", &values, &count); + * if (s == NATS_OK) + * { + * // do something with the values + * + * // then free the array of pointers. + * free((void*) values); + * } + * \endcode + * + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. + * + * @param h the pointer to the #natsHeader object. + * @param key the key for which the values are requested. + * @param values the memory location where the library will store the pointer to the array + * of values. + * @param count the memory location where the library will store the number of values returned. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. + */ +NATS_EXTERN natsStatus +natsHeader_Values(natsHeader *h, const char *key, const char* **values, int *count); + +/** \brief Get all header keys. + * + * \warning The returned strings are own by the library and MUST not be freed or altered. + * However, the returned array `keys` MUST be freed by the user. + * + * \code{.c} + * const char* *keys = NULL; + * int count = 0; + * + * s = natsMsgHeader_Keys(h, &keys, &count); + * if (s == NATS_OK) + * { + * // do something with the keys + * + * // then free the array of pointers. + * free((void*) keys); + * } + * \endcode + * + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. + * + * @param h the pointer to the #natsHeader object. + * @param keys the memory location where the library will store the pointer to the array + * of keys. + * @param count the memory location where the library will store the number of keys returned. + * @return #NATS_NOT_FOUND if no key is present. + */ +NATS_EXTERN natsStatus +natsHeader_Keys(natsHeader *h, const char* **keys, int *count); + +/** \brief Returns the number of keys. + * + * @param h the pointer to the #natsHeader object. + * @return The number of keys. If `h` is `NULL`, retunrs 0. + */ +NATS_EXTERN int +natsHeader_KeysCount(natsHeader *h); + +/** \brief Delete the value(s) associated with `key`. + * + * Delete the value(s) associated with `key`. + * + * \warning Headers are not thread-safe, that is, you must not set/add/get values or + * delete keys for the same message from different threads. + * + * @param h the pointer to the #natsHeader object. + * @param key the key to delete from the headers map. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. + */ +NATS_EXTERN natsStatus +natsHeader_Delete(natsHeader *h, const char *key); + +/** \brief Destroys the #natsHeader object. + * + * Releases memory allocated for this #natsHeader object. + * + * @param h the pointer to the #natsHeader object. + */ +NATS_EXTERN void +natsHeader_Destroy(natsHeader *h); + +/** @} */ // end of headerGroup + +/** \defgroup msgGroup Message + * + * NATS Message. + * @{ + */ + +/** \brief Destroys this list of messages. + * + * This function iterates through the list of all messages and call #natsMsg_Destroy + * for each valid (not set to `NULL`) message. It then frees the array that was + * allocated to hold pointers to those messages. + * + * \note The #natsMsgList object itself is not freed since it is expected that + * users will pass a pointer to a stack object. Should the user create its own + * object, it will be the user responsibility to free this object. + * + * @param list the #natsMsgList list of #natsMsg objects to destroy. + */ +NATS_EXTERN void +natsMsgList_Destroy(natsMsgList *list); + +/** \brief Creates a #natsMsg object. + * + * Creates a #natsMsg object. This is used by the subscription related calls + * and by #natsConnection_PublishMsg(). + * + * \note Messages need to be destroyed with #natsMsg_Destroy() when no + * longer needed. + * + * @see natsMsg_Destroy() + * + * @param newMsg the location where to store the pointer to the newly created * #natsMsg object. * @param subj the subject this message will be sent to. Cannot be `NULL`. * @param reply the optional reply for this message. @@ -4128,7 +4717,7 @@ natsMsgHeader_Add(natsMsg *msg, const char *key, const char *value); * @param key the key for which the value is requested. * @param value the memory location where the library will store the pointer to the first * value (if more than one is found) associated with the `key`. - * @return NATS_NOT_FOUND if `key` is not present in the headers. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. */ NATS_EXTERN natsStatus natsMsgHeader_Get(natsMsg *msg, const char *key, const char **value); @@ -4164,7 +4753,7 @@ natsMsgHeader_Get(natsMsg *msg, const char *key, const char **value); * @param values the memory location where the library will store the pointer to the array * of values. * @param count the memory location where the library will store the number of values returned. - * @return NATS_NOT_FOUND if `key` is not present in the headers. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. */ NATS_EXTERN natsStatus natsMsgHeader_Values(natsMsg *msg, const char *key, const char* **values, int *count); @@ -4196,7 +4785,7 @@ natsMsgHeader_Values(natsMsg *msg, const char *key, const char* **values, int *c * @param keys the memory location where the library will store the pointer to the array * of keys. * @param count the memory location where the library will store the number of keys returned. - * @return NATS_NOT_FOUND if no key is present. + * @return #NATS_NOT_FOUND if no key is present. */ NATS_EXTERN natsStatus natsMsgHeader_Keys(natsMsg *msg, const char* **keys, int *count); @@ -4212,7 +4801,7 @@ natsMsgHeader_Keys(natsMsg *msg, const char* **keys, int *count); * * @param msg the pointer to the #natsMsg object. * @param key the key to delete from the headers map. - * @return NATS_NOT_FOUND if `key` is not present in the headers. + * @return #NATS_NOT_FOUND if `key` is not present in the headers. */ NATS_EXTERN natsStatus natsMsgHeader_Delete(natsMsg *msg, const char *key); @@ -7759,13 +8348,923 @@ kvStatus_Destroy(kvStatus *sts); /** @} */ // end of kvGroup +/** \defgroup obsGroup Object Stores + * + * Object Stores offer a straightforward method for storing large objects + * within JetStream. These stores are backed by a specially configured streams, + * designed to efficiently and compactly store these objects. + * + * The Object Store, also known as a bucket, enables the execution of various + * operations: + * + * - create an object store + * - update an object store + * - get an object store + * - delete an object store + * - list all objects in a bucket + * - watch for changes on objects in a bucket + * - create links to other objects or other buckets + * + * \warning Object Stores needs TLS to be able to generate and check SHA256 object + * digests. If the library is compiled without TLS support, some of the features + * will still work, but "put" and "get" operations will fail. + * + * @{ + */ + +/** \defgroup obsGroupMgt Object Stores management + * + * These functions allow to create, get or delete an Object Store. + * + * @{ + */ + +/** \brief Initializes an Object Store configuration structure. + * + * Use this before setting specific #objStoreConfig options and passing it to #js_CreateObjectStore. + * + * @see js_CreateObjectStore + * + * @param cfg the pointer to the stack variable #objStoreConfig to initialize. + */ +NATS_EXTERN natsStatus +objStoreConfig_Init(objStoreConfig *cfg); + +/** \brief Creates an object store with a given configuration. + * + * Creates an object store with a given configuration. + * + * Bucket names are restricted to this set of characters: `A-Z`, `a-z`, `0-9`, `_` and `-`. + * + * \note The returned #objStore object needs to be destroyed using #objStore_Destroy when + * no longer needed to free allocated memory. This is different from deleting an object store + * from the server using the #js_DeleteObjectStore API. + * + * @param new_obs the location where to store the pointer to the #objStore object. + * @param js the pointer to the #jsCtx object. + * @param cfg the pointer to the #objStoreConfig configuration information used to create the #objStore object. + */ +NATS_EXTERN natsStatus +js_CreateObjectStore(objStore **new_obs, jsCtx *js, objStoreConfig *cfg); + +/** \brief Updates an object store with a given configuration. + * + * Updates an object store with a given configuration. + * + * Bucket names are restricted to this set of characters: `A-Z`, `a-z`, `0-9`, `_` and `-`. + * + * \note If the object store with given name does not exist, this will return #NATS_NOT_FOUND. + * + * \note The returned #objStore object needs to be destroyed using #objStore_Destroy when + * no longer needed to free allocated memory. This is different from deleting an object store + * from the server using the #js_DeleteObjectStore API. + * + * @param new_obs the location where to store the pointer to the #objStore object. + * @param js the pointer to the #jsCtx object. + * @param cfg the pointer to the #objStoreConfig configuration information used to create the #objStore object. + */ +NATS_EXTERN natsStatus +js_UpdateObjectStore(objStore **new_obs, jsCtx *js, objStoreConfig *cfg); + +/** \brief Looks-up and binds to an existing object store. + * + * This call is when the user wants to use an existing object store. + * If the store does not already exists, an error is returned. + * + * Bucket names are restricted to this set of characters: `A-Z`, `a-z`, `0-9`, `_` and `-`. + * + * \note The returned #objStore object needs to be destroyed using #objStore_Destroy when + * no longer needed to free allocated memory. This is different from deleting an object store + * from the server using the #js_DeleteObjectStore API. + * + * @param new_obs the location where to store the pointer to the #objStore object. + * @param js the pointer to the #jsCtx object. + * @param bucket the name of the bucket of the existing object store. + */ +NATS_EXTERN natsStatus +js_ObjectStore(objStore **new_obs, jsCtx *js, const char *bucket); + +/** \brief Retrieves a list of bucket names. + * + * Retrieves the list of all object store names. + * + * \note The returned #objStoreNamesList object needs to be destroyed using #objStoreNamesList_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreNamesList_Destroy + * + * @param new_list the location where to store the pointer to the #objStoreNamesList object. + * @param js the pointer to the #jsCtx object. + */ +NATS_EXTERN natsStatus +js_ObjectStoreNames(objStoreNamesList **new_list, jsCtx *js); + +/** \brief Destroys the object store names list object. + * + * Releases memory allocated for this list of object store names. + * + * \warning All strings contained in the list will become invalid after this call. + * + * @param list the pointer to the #objStoreNamesList object. + */ +NATS_EXTERN void +objStoreNamesList_Destroy(objStoreNamesList *list); + +/** \brief Retrieves a list of bucket statuses. + * + * Retrieves the list of all object store statuses. + * + * \note The returned #objStoreStatusesList object needs to be destroyed using #objStoreStatusesList_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreStatusesList_Destroy + * + * @param new_list the location where to store the pointer to the #objStoreStatusesList object. + * @param js the pointer to the #jsCtx object. + */ +NATS_EXTERN natsStatus +js_ObjectStoreStatuses(objStoreStatusesList **new_list, jsCtx *js); + +/** \brief Destroys the object store statuses list object. + * + * Releases memory allocated for this list of object store statuses. + * + * \warning All #objStoreStatus objects contained in the list will become invalid after this call. + * + * @param list the pointer to the #objStoreStatusesList object. + */ +NATS_EXTERN void +objStoreStatusesList_Destroy(objStoreStatusesList *list); + +/** \brief Deletes an object store. + * + * This will delete the object store with the `bucket` name. + * + * Bucket names are restricted to this set of characters: `A-Z`, `a-z`, `0-9`, `_` and `-`. + * + * @param js the pointer to the #jsCtx object. + * @param bucket the name of the bucket of the existing object store. + */ +NATS_EXTERN natsStatus +js_DeleteObjectStore(jsCtx *js, const char *bucket); + +/** @} */ // end of obsGroupMgt + +/** \defgroup obsMgt Object Store management + * + * These functions allow to get information, status, etc... on a given object store. + * + * @{ + */ + +/** \brief Initializes an Object Store options structure. + * + * Use this before setting specific #objStoreOptions options and passing it to the APIs + * that accept such object. + * + * @param opts the pointer to the stack variable #objStoreOptions to initialize. + */ +NATS_EXTERN natsStatus +objStoreOptions_Init(objStoreOptions *opts); + +/** \brief Retrieves the current information for the object. + * + * Retrieves the current information for the object, containing + * the object's metadata and instance information. + * + * If the object does not exist, #NATS_NOT_FOUND will be returned. + * + * The #objStoreOptions.ShowDeleted option can be supplied to return + * an object even if it was marked as deleted. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * If `opts` is not `NULL`, the #objStoreOptions.ShowDeleted can be used to get information about + * an object within the store, even if it has been deleted. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to retrieve the info for. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_GetInfo(objStoreInfo **new_info, objStore *obs, const char *name, objStoreOptions *opts); + +/** \brief Destroys the ObjectStore information object. + * + * Releases memory allocated for this #objStoreInfo object. + * + * @param info the pointer to the #objStoreInfo object to destroy. + */ +NATS_EXTERN void +objStoreInfo_Destroy(objStoreInfo *info); + +/** \brief Updates the metadata for the object. + * + * This will update the metadata for the object. + * + * The status #NATS_ILLEGAL_STATE may be returned if: + * - the object does not exist. + * - the new name is different from the old one, and an object with the new + * name already exists. + * + * The error string will add more context. + * + * @param obs the pointer to the #objStore object. + * @param name the name of the object. + * @param meta the pointer to the #objStoreMeta information used for the update. + */ +NATS_EXTERN natsStatus +objStore_UpdateMeta(objStore *obs, const char *name, objStoreMeta *meta); + +/** \brief Deletes the named object from the object store. + * + * This will delete the named object from the object store. If the object + * does not exist, the error #NATS_NOT_FOUND will be returned. If the object is + * already deleted, no error will be returned. + * + * All chunks for the object will be purged, and the object will be marked as + * deleted. + * + * @param obs the pointer to the #objStore object. + * @param name the name of the object to delete. + */ +NATS_EXTERN natsStatus +objStore_Delete(objStore *obs, const char *name); + +/** \brief Adds a link to another object. + * + * A link is a reference to another object. The provided name is the name of the link object. + * The provided #objStoreInfo is the info of the object being linked to. + * + * The error #NATS_ILLEGAL_STATE is returned if an object with given name already exists, + * or if the provided object is a link. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param obs the pointer to the #objStore object. + * @param name the name of the link. + * @param obj the pointer to the #objStoreInfo object representing the object being linked to. + */ +NATS_EXTERN natsStatus +objStore_AddLink(objStoreInfo **new_info, objStore *obs, const char *name, objStoreInfo *obj); + +/** \brief Adds a link to another object store. + * + * A link is a reference to another object store. The provided name is the name of + * the link object. + * + * The provided #objStore is the object store being linked to. + * + * The error #NATS_ILLEGAL_STATE is returned if an object with given name already exists. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param obs the pointer to the #objStore object. + * @param name the name of the link. + * @param bucket the pointer to the #objStore object representing the object store being linked to. + */ +NATS_EXTERN natsStatus +objStore_AddBucketLink(objStoreInfo **new_info, objStore *obs, const char *name, objStore *bucket); + +/** \brief Seals the object store. + * + * This function will seal the object store, no further modifications will be allowed. + * + * @param obs the pointer to the #objStore object. + */ +NATS_EXTERN natsStatus +objStore_Seal(objStore *obs); + +/** \brief Initializes the object store watcher options object + * + * Use this before setting specific object store watcher options and passing it + * to #objStore_Watch. + * + * @see objStore_Watch + * + * @param opts the pointer to the #objStoreWatchOptions object. + */ +NATS_EXTERN natsStatus +objStoreWatchOptions_Init(objStoreWatchOptions *opts); + +/** \brief Watches for updates to objects in the store. + * + * This will create a watcher for any updates to objects in the store. By default, the watcher will + * send the latest information for each object and all future updates. The call + * to #objStoreWatcher_Next will return a `NULL` #objStoreInfo with status #NATS_OK + * when it has received all initial values. + * + * The watcher can be configured by initializing a #objStoreWatchOptions structure + * with #objStoreWatchOptions_Init and setting the following properties: + * - IgnoreDeletes will have the watcher not pass any objects with delete markers. + * - UpdatesOnly will have the watcher only pass updates on objects (without latest info when started). + * + * \note The returned #objStoreWatcher object needs be destroyed using #objStoreWatcher_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreWatchOptions_Init + * @see objStoreWatcher_Next + * @see objStoreWatcher_Stop + * @see objStoreWatcher_Destroy + * + * @param new_watcher the location where to store the pointer to the #objStoreWatcher object. + * @param obs the pointer to the #objStore object. + * @param opts the pointer to the #objStoreWatchOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_Watch(objStoreWatcher **new_watcher, objStore *obs, objStoreWatchOptions *opts); + +/** \brief Returns the next object store information for this object store watcher. + * + * Returns the next object store information for this watcher. The `new_info` may be `NULL` + * (with #NATS_OK status) to indicate that the initial state has been retrieved. + * + * If a thread is waiting on this call, it can be canceled with a call to + * #objStoreWatcher_Stop. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * @see objStoreWatcher_Stop + * @see objStoreWatcher_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object. + * @param watcher the pointer to the #objStoreWatcher object. + * @param timeout how long to wait (in milliseconds) for the next entry. + */ +NATS_EXTERN natsStatus +objStoreWatcher_Next(objStoreInfo **new_info, objStoreWatcher *watcher, int64_t timeout); + +/** \brief Stops the object store watcher. + * + * Stops the watcher. + * + * \note Stopping a stopped watcher returns #NATS_OK. + * + * \warning After this call, new and existing calls to #objStoreWatcher_Next (that are waiting + * for an update) will return with #NATS_ILLEGAL_STATE. + * + * @param watcher the pointer to the #objStoreWatcher object. + */ +NATS_EXTERN natsStatus +objStoreWatcher_Stop(objStoreWatcher *watcher); + +/** \brief Destroys the object store's watcher object. + * + * Releases memory allocated for this #objStoreWatcher object. + * + * @param watcher the pointer to the #objStoreWatcher object. + */ +NATS_EXTERN void +objStoreWatcher_Destroy(objStoreWatcher *watcher); + +/** \brief Lists information about objects in the object store. + * + * This function will return a #objStoreInfoList object that contains the + * #objStoreInfo for objects in the store. + * + * If the object store is empty, this function will return #NATS_NOT_FOUND. + * + * To get information about deleted objects, initialize the options with #objStoreOptions_Init, + * then set #objStoreOptions.ShowDeleted to true and pass the options to this call. + * + * \note The returned #objStoreInfoList object needs be destroyed using #objStoreInfoList_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfoList_Destroy + * + * @param new_list the location where to store the pointer to the #objStoreInfoList object. + * @param obs the pointer to the #objStore object. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_List(objStoreInfoList **new_list, objStore *obs, objStoreOptions *opts); + +/** \brief Destroys the list of object informations. + * + * Releases memory allocated for this list of object store informations. + * + * \warning All #objStoreInfo objects contained in the list will become invalid after this call. + * + * @param list the pointer to the #objStoreStatusesList object. + */ +NATS_EXTERN void +objStoreInfoList_Destroy(objStoreInfoList *list); + +/** \brief Retreive the status and configuration of the bucket. + * + * This function retrieves the status and configuration of the bucket. + * + * \note The returned #objStoreStatus object needs be destroyed using #objStoreStatus_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreStatus_Destroy + * + * @param new_status the location where to store the pointer to the #objStoreStatus object. + * @param obs the pointer to the #objStore object. + */ +NATS_EXTERN natsStatus +objStore_Status(objStoreStatus **new_status, objStore *obs); + +/** \brief Destroys the object store status object. + * + * Releases memory allocated for this object store status object. + * + * @param status the pointer to the #objStoreStatus object. + */ +NATS_EXTERN void +objStoreStatus_Destroy(objStoreStatus *status); + +/** \brief Destroys an object store object. + * + * This will simply free memory resources in the library for this object store object, + * but does not delete the object store in the server. + * + * @param obs the pointer to the #objStore object. + */ +NATS_EXTERN void +objStore_Destroy(objStore *obs); + +/** @} */ // end of obsMgt + +/** \brief Initializes an object store meta structure. + * + * Typically, the user will declare an #objStoreMeta variable on the stack and call + * this function to initializes the structure, set some fields and pass it to an API. + * + * \code{.unparsed} + * objStoreMeta meta; + * + * objStoreMeta_Init(&meta); + * meta.Name = "test"; + * s = objStore_Put(&put, obs, &meta); + * ... + * \endcode + * + * @see #objStore_Put + * + * @param meta the pointer to the #objStoreMeta structure to initialize. + */ +NATS_EXTERN natsStatus +objStoreMeta_Init(objStoreMeta *meta); + +/** \brief Initiates a session to put bytes into an object. + * + * If the object already exists, it will be overwritten. The object name is + * required and taken from the #objStoreMeta.Name field. + * + * On success, an #objStorePut object will be returned. This should then be passed + * to the functions to add and complete the put operation. + * + * \note The returned #objStorePut object needs be destroyed using #objStorePut_Destroy + * when no longer needed to free allocated memory. + * + * \code{.unparsed} + * objStoreMeta meta; + * objStorePut *put = NULL; + * objStoreInfo *info = NULL; + * + * objStoreMeta_Init(&meta); + * meta.Name = "test"; + * s = objStore_Put(&put, obs, &meta); + * if (s == NATS_OK) + * { + * natsStatus addSts = NATS_OK; + * + * while ((addSts == NATS_OK) && hasMoreDataToAdd) + * { + * // Get some data from somewhere... + * addSts = objStorePut_Add(put, data, dataLen); + * // If data needs to be freed, it is safe to do it after the "Add" call. + * ... + * } + * // We are done adding data, call "Complete". We should invoke this even + * // if there were issues adding data. The call will then purge partial chunks. + * // The last param is the timeout for operation to complete, here using 3 seconds. + * s = objStorePut_Complete(&info, put, 3000); + * } + * // At this point, the "put" operation is complete. On success, we would have an `info` + * // variable that can be inspected. If not needed, `NULL` should have been passed to + * // the `objStorePut_Complete` call. + * if (s == NATS_OK) + * { + * // Inspect the info if needed. + * } + * // Now we need to destroy both object. No need to check for `NULL` (as long as you + * properly initialized them to `NULL`) since the calls internally check for that. + * objStoreInfo_Destroy(info); + * objStorePut_Destroy(put); + * ... + * \endcode + * + * @see objStoreMeta_Init + * @see objStorePut_Add + * @see objStorePut_Complete + * @see objStorePut_Destroy + * + * @param new_put the location where to store the pointer to the #objStorePut object. + * @param obs the pointer to the #objStore object. + * @param meta the pointer to the #objStoreMeta object. + */ +NATS_EXTERN natsStatus +objStore_Put(objStorePut **new_put, objStore *obs, objStoreMeta *meta); + +/** \brief Add data to the object. + * + * After successfully calling #objStore_Put, the returned #objStorePut object + * is used by this function to add bytes to the object. When there is no more data + * to add, call #objStorePut_Complete to complete the operation. + * + * If an error occurs, invoking this function again will immediately error out. + * The only outcome is to call #objStorePut_Complete to undo the partial additions + * and free the object. + * + * See #objStore_Put for an example on how to use the set of APIs. + * + * @see objStore_Put + * @see objStorePut_Complete + * + * @param put the pointer to the #objStorePut object. + * @param data the pointer to the byte array to add to the object. + * @param dataLen the number of bytes of the byte array to add. + */ +NATS_EXTERN natsStatus +objStorePut_Add(objStorePut *put, const void *data, int dataLen); + +/** \brief Complete a put operation. + * + * After adding data, this call will complete the operation and on success return + * the #objStoreInfo (if desired). + * + * If there were errors adding the data, this function will purge the partial + * data added. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param put the pointer to the #objStorePut object. + * @param timeout the amount of time (in milliseconds) to wait for the operation to complete. + */ +NATS_EXTERN natsStatus +objStorePut_Complete(objStoreInfo **new_info, objStorePut *put, int64_t timeout); + +/** \brief Destroys the object store's put object + * + * Releases memory allocated for this #objStorePut object. + * + * \note If there were errors and the put operation did not complete, this function will + * attempt to purge the data that was partially added. + * + * @param put the pointer to the #objStorePut object. + */ +NATS_EXTERN void +objStorePut_Destroy(objStorePut *put); + +/** \brief Put a string into this object. + * + * This is a convenience function to put a string into this object + * store under the given name. + * + * An #objStoreInfo will be returned, containing the object's metadata, digest + * and instance information. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to put the string into. + * @param data the string to put into the object. + */ +NATS_EXTERN natsStatus +objStore_PutString(objStoreInfo **new_info, objStore *obs, const char *name, const char *data); + +/** \brief Put bytes into this object. + * + * This is a convenience function to put bytes into this object + * store under the given name. + * + * An #objStoreInfo will be returned, containing the object's metadata, digest + * and instance information. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to put the string into. + * @param data the bytes to put into the object. + * @param dataLen the number of bytes to put into the object. + */ +NATS_EXTERN natsStatus +objStore_PutBytes(objStoreInfo **new_info, objStore *obs, const char *name, const void *data, int dataLen); + +/** \brief Put the content of a file into this object. + * + * This is a convenience function to put a file content into this object store. + * The name of the object will be the path of the file. + * + * \note The returned #objStoreInfo object needs be destroyed using #objStoreInfo_Destroy + * when no longer needed to free allocated memory. + * + * @see objStoreInfo_Destroy + * + * @param new_info the location where to store the pointer to the #objStoreInfo object, or `NULL` if not needed. + * @param obs the pointer to the #objStore object. + * @param fileName the name of the file whose content will be put into the object. + */ +NATS_EXTERN natsStatus +objStore_PutFile(objStoreInfo **new_info, objStore *obs, const char *fileName); + +/** \brief Pull the named object from the object store. + * + * If the object does not exist, #NATS_NOT_FOUND will be returned. + * + * The returned #objStoreGet object will contain the object's metadata and the user + * can call #objStoreGet_Read to get some bytes as they become available, or + * #objStoreGet_ReadAll to get the whole object in one call. + * + * The option #objStoreOptions.ShowDeleted can be provided to return an object + * even if it was marked as deleted. + * + * \note The returned #objStoreGet object needs be destroyed using #objStoreGet_Destroy + * when no longer needed to free allocated memory. + * + * Here is an example to read data by chunks: + * + * \code{.unparsed} + * objStoreGet *get = NULL; + * + * s = objStore_Get(&get, obs, "myobject", NULL); + * if (s == NATS_OK) + * { + * objStoreInfo *info = NULL; + * bool done = false; + * + * // We can inspect the info if we chose to do so. + * s = objStoreGet_Info(&info); + * if (s == NATS_OK) + * { + * // Inspect the info... + * } + * // DO NOT destroy the info, it belongs to the `objStoreGet` object. + * + * // Get data in chunks. + * while ((s == NATS_OK) && !done) + * { + * void *data = NULL; + * int len = 0; + * + * // Read some data. The last param is a timeout (in milliseconds) to wait + * // for some data to become available. + * s = objStoreGet_Read(&done, &data, &len, get, 4000); + * if (s == NATS_OK) + * { + * // Do something with the data + * ... + * // It is the user responsibility to free the memory. + * free(data); + * } + * } + * // At this point, all data for the object has been retrieved. + * } + * // Now we need to destroy the object. No need to check for `NULL` (as long as you + * properly initialized it to `NULL`) since the call internally check for that. + * objStoreGet_Destroy(get); + * ... + * \endcode + * + * Here is an example to read data all at once. Unless the user needs to inspect + * the object store information's object prior to read the data, the convenience + * functtion #objStore_GetBytes will be equivalent much simpler to use. + * + * \code{.unparsed} + * objStoreGet *get = NULL; + * + * s = objStore_Get(&get, obs, "myobject", NULL); + * if (s == NATS_OK) + * { + * objStoreInfo *info = NULL; + * + * // We can inspect the info if we chose to do so. + * s = objStoreGet_Info(&info); + * if (s == NATS_OK) + * { + * // Inspect the info... + * } + * // DO NOT destroy the info, it belongs to the `objStoreGet` object. + * + * if (s == NATS_OK) + * { + * void *data = NULL; + * int len = 0; + * + * // The last param is a timeout (in milliseconds) to wait + * // for some data to become available. Here we are using 10 seconds. + * s = objStoreGet_ReadAll(&data, &len, get, 10000); + * if (s == NATS_OK) + * { + * // Do something with the data + * ... + * // It is the user responsibility to free the memory. + * free(data); + * } + * } + * } + * // Now we need to destroy the object. No need to check for `NULL` (as long as you + * properly initialized it to `NULL`) since the call internally check for that. + * objStoreGet_Destroy(get); + * ... + * \endcode + * + * @see objStoreGet_Read + * @see objStoreGet_ReadAll + * @see objStoreGet_Destroy + * + * @param new_get the location where to store the pointer to the #objStoreGet object. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to pull data from. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_Get(objStoreGet **new_get, objStore *obs, const char *name, objStoreOptions *opts); + +/** \brief Returns a handle to the information object own by the #objStoreGet object. + * + * Allows the user to get information about the pulled object. + * + * \warning The #objStoreInfo is owned by the #objStoreGet object and MUST NOT be destroyed + * or altered by the user. In other words, do not call #objStoreInfo_Destroy on the #objStoreInfo + * pointer returned by this function. Instead, call #objStoreGet_Destroy to free allocated memory. + * + * @param new_info the location where to store the pointer to the #objStoreInfo object. + * @param get the pointer to the #objStoreGet object owning this information object. + */ +NATS_EXTERN natsStatus +objStoreGet_Info(const objStoreInfo **new_info, objStoreGet *get); + +/** \brief Returns some bytes of the pulled object. + * + * This call returns up to a chunk of bytes of the object. The `dataLen` will be set with + * the number of bytes that were returned. + * + * The `done` boolean will be set to `true` if the call detects that this was the last + * bit of data that consistuted this object. Calling this function again would result + * in the #NATS_ILLEGAL_STATE error returned. + * + * \warning The returned `data` must be freed by the user. + * + * See #objStore_Get for example on how to use the set of APIs. + * + * @see objStore_Get + * + * @param done the location where to store the boolean indicating if this is the last read. + * @param new_data the location where to store the pointer to a chunk of the object's data. + * @param dataLen the location where to store the number of bytes returned. + * @param get the pointer to the #objStoreGet object. + * @param timeout the amount of time (in milliseconds) allowed to perform the operation. + */ +NATS_EXTERN natsStatus +objStoreGet_Read(bool *done, void **new_data, int *dataLen, objStoreGet *get, int64_t timeout); + +/** \brief Returns the remaining bytes of the pulled object. + * + * This call returns all data representing the object, or the remaining of the data if some + * were already collected using #objStoreGet_Read. + * + * This function should be called only once (except in the case of a #NATS_TIMEOUT), if not + * the error #NATS_ILLEGAL_STATE error will be returned. + * + * \warning The returned `data` must be freed by the user. + * + * See #objStore_Get for example on how to use the set of APIs. + * + * @see objStore_Get + * + * @param new_data the location where to store the pointer to a chunk of the object's data. + * @param dataLen the location where to store the number of bytes returned. + * @param get the pointer to the #objStoreGet object. + * @param timeout the amount of time (in milliseconds) allowed to perform the operation. + */ +NATS_EXTERN natsStatus +objStoreGet_ReadAll(void **new_data, int *dataLen, objStoreGet *get, int64_t timeout); + +/** \brief Destroys the object store's get object. + * + * Releases memory allocated for this #objStoreGet object. + * + * @param get the pointer to the #objStoreGet object. + */ +NATS_EXTERN void +objStoreGet_Destroy(objStoreGet *get); + +/** \brief Pull the named object from the object store and return it as a string. + * + * This is a convenience function to pull an object from this object store and return + * it as a string. + * + * If the object does not exist, #NATS_NOT_FOUND will be returned. + * + * The option #objStoreOptions.ShowDeleted can be provided to return an object + * even if it was marked as deleted. + * + * Here is an example: + * + * * \code{.unparsed} + * objStoreOptions o; + * + * objStoreOptions_Init(&o); + * s = objStore_GetString(&str, obs, "myobject", &o); + * if (s == NATS_OK) + * { + * // Do something with the string + * ... + * // It is the user responsibility to free the memory. + * free(str); + * } + * ... + * \endcode + * + * \note The string is NULL terminated, therefore the allocated memory is one byte more + * than the size reported in the object's #objStoreInfo.Size field. + * + * \warning The returned string must be freed when no longer needed to reclaim memory. + * + * @param new_str the location where to store the pointer to the retrieved string. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to pull the string from. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_GetString(char **new_str, objStore *obs, const char *name, objStoreOptions *opts); + +/** \brief Pull the named object from the object store and return it as a byte array. + * + * This is a convenience function to pull an object from this object store and return + * it as a byte array. + * + * If the object does not exist, #NATS_NOT_FOUND will be returned. + * + * The option #objStoreOptions.ShowDeleted can be provided to return an object + * even if it was marked as deleted. See #objStore_GetString for an example on how to do so. + * + * \warning The returned byte array must be freed when no longer needed to reclaim memory. + * + * @param new_data the location where to store the pointer to the retrieved byte array. + * @param dataLen the location where to store the number of bytes of the byte array. + * @param obs the pointer to the #objStore object. + * @param name the name of the object to pull the string from. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_GetBytes(void **new_data, int *dataLen, objStore *obs, const char *name, objStoreOptions *opts); + +/** \brief Pull the named object from the object store and place it into a file. + * + * This is a convenience function to pull an object from this object store and place + * it in a file. If the file already exists, it will be overwritten, otherwise it will + * be created. + * + * If the object does not exist, #NATS_NOT_FOUND will be returned. + * + * The option #objStoreOptions.ShowDeleted can be provided to return an object + * even if it was marked as deleted. See #objStore_GetString for an example on how to do so. + * + * @param obs the pointer to the #objStore object. + * @param name the name of the object to pull the string from. + * @param fileName the name of the file to write the content into. + * @param opts the pointer to the #objStoreOptions object, possibly `NULL`. + */ +NATS_EXTERN natsStatus +objStore_GetFile(objStore *obs, const char *name, const char *fileName, objStoreOptions *opts); + +/** @} */ // end of obsGroup + /** @} */ // end of funcGroup // // Microservices. // -/** \defgroup microGroup - Microservices +/** \defgroup microGroup Microservices * * ### NATS Microservices. * diff --git a/src/natsp.h b/src/natsp.h index b576a2d93..e4bdceffa 100644 --- a/src/natsp.h +++ b/src/natsp.h @@ -25,12 +25,17 @@ #include #include #include +#include +#define nats_hash EVP_MD_CTX +#define NATS_HASH_MAX_LEN EVP_MAX_MD_SIZE #else -#define SSL void* -#define SSL_free(c) { (c) = NULL; } -#define SSL_CTX void* -#define SSL_CTX_free(c) { (c) = NULL; } -#define NO_SSL_ERR "The library was built without SSL support!" +#define SSL void* +#define SSL_free(c) { (c) = NULL; } +#define SSL_CTX void* +#define SSL_CTX_free(c) { (c) = NULL; } +#define NO_SSL_ERR "The library was built without SSL support!" +#define nats_hash void +#define NATS_HASH_MAX_LEN 32 #endif #include "err.h" @@ -106,6 +111,8 @@ #define nats_IsStringEmpty(s) ((((s) == NULL) || ((s)[0] == '\0')) ? true : false) #define nats_HasPrefix(_s, _prefix) (nats_IsStringEmpty(_s) ? nats_IsStringEmpty(_prefix) : (strncmp((_s), (_prefix), strlen(_prefix)) == 0)) +#define __natsHeader natsStrHash + static inline bool nats_StringEquals(const char *s1, const char *s2) { if (s1 == NULL) @@ -147,6 +154,7 @@ static inline bool nats_StringEquals(const char *s1, const char *s2) #define NATS_MILLIS_TO_NANOS(d) (((int64_t)d)*(int64_t)1E6) #define NATS_SECONDS_TO_NANOS(d) (((int64_t)d)*(int64_t)1E9) +#define NATS_NANOS_TO_MILLIS(d) (((int64_t)d)/(int64_t)1E6) #if __STDC_VERSION__ >= 201112L #define NATS_GLOBAL_STATIC_ASSERT(_cond, _message_as_id) \ @@ -370,6 +378,8 @@ typedef struct __pmInfo } pmInfo; +typedef void (*js_onReleaseCb)(void *arg); + struct __jsCtx { natsMutex *mu; @@ -388,6 +398,8 @@ struct __jsCtx int64_t pmcount; int stalled; bool closed; + js_onReleaseCb onReleaseCb; + void *onReleaseCbArg; }; typedef struct __jsFetch @@ -527,6 +539,60 @@ struct __kvWatcher }; +struct __objStore +{ + natsMutex *mu; + int refs; + jsCtx *js; + jsCtx *pushJS; + char *name; + char *streamName; + +}; + +struct __objStorePut +{ + natsMutex *mu; + int refs; + objStore *obs; + char *echunkSubj; + objStoreInfo *info; + jsCtx *pubJS; + char *metaSubj; + char *chunkSubj; + nats_hash *h; + uint32_t sent; + uint64_t total; + natsStatus err; + char *errTxt; + bool pcof; + +}; + +struct __objStoreGet +{ + objStore *obs; + objStoreInfo *info; + nats_hash *digest; + natsSubscription *sub; + uint64_t remaining; + bool done; + +}; + +struct __objStoreWatcher +{ + natsMutex *mu; + int refs; + objStore *obs; + natsSubscription *sub; + bool initDone; + bool retMarker; + bool ignoreDel; + bool stopped; + +}; + typedef struct __natsSubscriptionControlMessages { struct @@ -804,6 +870,25 @@ nats_sslRegisterThreadForCleanup(void); void nats_setNATSThreadKey(void); +// +// SHA256 +// +void +nats_hashNoErrorOnNoSSL(bool noError); + +natsStatus +nats_hashNew(nats_hash **new_hash); + +natsStatus +nats_hashWrite(nats_hash *hash, const void *data, int dataLen); + +natsStatus +nats_hashSum(nats_hash *hash, unsigned char *digest, unsigned int *len); + +void +nats_hashDestroy(nats_hash *hash); + + // // Threads // diff --git a/src/object.c b/src/object.c new file mode 100644 index 000000000..76b4408e9 --- /dev/null +++ b/src/object.c @@ -0,0 +1,2510 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "natsp.h" +#include "object.h" +#include "js.h" + +static const char *objBackingStore = "JetStream"; +static const char *objNamePrefix = "OBJ_"; +static const char *objAllChunksFilter = "$O.*.C.>"; // filter to get all stream names/info for object stores +static const char *objNameTmpl = "OBJ_%s"; // OBJ_ // stream name +static const char *objAllChunksPreTmpl = "$O.%s.C.>"; // $O..C.> // chunk stream subject +static const char *objAllMetaPreTmpl = "$O.%s.M.>"; // $O..M.> // meta stream subject +static const char *objChunksPreTmpl = "$O.%s.C.%s"; // $O..C. // chunk message subject +static const char *objMetaPreTmpl = "$O.%s.M.%s"; // $O..M. // meta message subject +static const char *objDigestTmpl = "SHA-256=%s"; + + +////////////////////////////////////////////////////////////////////////////// +// objStore management APIs +////////////////////////////////////////////////////////////////////////////// + +natsStatus +objStoreConfig_Init(objStoreConfig *cfg) +{ + if (cfg == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + memset(cfg, 0, sizeof(objStoreConfig)); + return NATS_OK; +} + +static void +_freeObs(objStore *obs) +{ + jsCtx *js = NULL; + jsCtx *pushJS = NULL; + + if (obs == NULL) + return; + + js = obs->js; + pushJS = obs->pushJS; + NATS_FREE(obs->name); + NATS_FREE(obs->streamName); + natsMutex_Destroy(obs->mu); + NATS_FREE(obs); + js_release(pushJS); + js_release(js); +} + +static void +_retainObs(objStore *obs) +{ + natsMutex_Lock(obs->mu); + obs->refs++; + natsMutex_Unlock(obs->mu); +} + +static void +_releaseObs(objStore *obs) +{ + bool doFree; + + if (obs == NULL) + return; + + natsMutex_Lock(obs->mu); + doFree = (--(obs->refs) == 0); + natsMutex_Unlock(obs->mu); + + if (doFree) + _freeObs(obs); +} + +static natsStatus +_createObjStore(objStore **new_obs, jsCtx *js, const char *bucket) +{ + natsStatus s = NATS_OK; + objStore *obs = NULL; + + if (!nats_validBucketName(bucket)) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrInvalidStoreName); + + obs = (objStore*) NATS_CALLOC(1, sizeof(objStore)); + if (obs == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + obs->refs = 1; + s = natsMutex_Create(&(obs->mu)); + IF_OK_DUP_STRING(s, obs->name, bucket); + if ((s == NATS_OK) && (nats_asprintf(&(obs->streamName), objNameTmpl, bucket) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + obs->js = js; + js_retain(js); + *new_obs = obs; + } + else + _freeObs(obs); + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_addOrUpdate(objStore **new_obs, jsCtx *js, objStoreConfig *cfg, bool add) +{ + natsStatus s = NATS_OK; + objStore *obs = NULL; + char *chunks = NULL; + char *meta = NULL; + const char *subjects[2]; + jsStreamConfig scfg; + + if ((new_obs == NULL) || (js == NULL) || (cfg == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + s = _createObjStore(&obs, js, cfg->Bucket); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + + if (nats_asprintf(&chunks, objAllChunksPreTmpl, cfg->Bucket) < 0) + s = nats_setDefaultError(NATS_NO_MEMORY); + if ((s == NATS_OK) && (nats_asprintf(&meta, objAllMetaPreTmpl, cfg->Bucket) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + subjects[0] = chunks; + subjects[1] = meta; + + int replicas = cfg->Replicas; + if (replicas == 0) + replicas = 1; + + int64_t maxBytes = cfg->MaxBytes; + if (maxBytes == 0) + maxBytes = -1; + + jsStorageCompression compression = js_StorageCompressionNone; + if (cfg->Compression) + compression = js_StorageCompressionS2; + + jsStreamConfig_Init(&scfg); + scfg.Name = obs->streamName; + scfg.Description = cfg->Description; + scfg.Subjects = subjects; + scfg.SubjectsLen = 2; + scfg.MaxAge = NATS_MILLIS_TO_NANOS(cfg->TTL); + scfg.MaxBytes = maxBytes; + scfg.Storage = cfg->Storage; + scfg.Replicas = (int64_t) replicas; + scfg.Placement = cfg->Placement; + scfg.Discard = js_DiscardNew; + scfg.AllowRollup = true; + scfg.AllowDirect = true; + scfg.Compression = compression; + scfg.Metadata = cfg->Metadata; + } + if (s == NATS_OK) + { + jsErrCode errCode = 0; + + if (add) + { + s = js_AddStream(NULL, js, &scfg, NULL, &errCode); + if ((s != NATS_OK) && (errCode == JSStreamNameExistErr)) + s = nats_setError(NATS_ERR, "%s: %s", obsErrBucketExists, cfg->Bucket); + } + else + { + s = js_UpdateStream(NULL, js, &scfg, NULL, &errCode); + if ((s != NATS_OK) && (errCode == JSStreamNotFoundErr)) + s = nats_setError(NATS_NOT_FOUND, "%s: %s", obsErrBucketNotFound, cfg->Bucket); + } + } + if (s == NATS_OK) + { + js_lock(js); + s = natsConnection_JetStream(&(obs->pushJS), js->nc, &(js->opts)); + js_unlock(js); + } + + NATS_FREE(chunks); + NATS_FREE(meta); + + if (s == NATS_OK) + *new_obs = obs; + else + _freeObs(obs); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +js_CreateObjectStore(objStore **new_obs, jsCtx *js, objStoreConfig *cfg) +{ + natsStatus s = _addOrUpdate(new_obs, js, cfg, true); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +js_UpdateObjectStore(objStore **new_obs, jsCtx *js, objStoreConfig *cfg) +{ + natsStatus s = _addOrUpdate(new_obs, js, cfg, false); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +js_ObjectStore(objStore **new_obs, jsCtx *js, const char *bucket) +{ + natsStatus s = NATS_OK; + objStore *obs = NULL; + jsStreamInfo *si = NULL; + jsErrCode errCode = 0; + + if ((new_obs == NULL) || (js == NULL) || nats_IsStringEmpty(bucket)) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (!nats_validBucketName(bucket)) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrInvalidStoreName); + + s = _createObjStore(&obs, js, bucket); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + + // Lookup the stream. + s = js_GetStreamInfo(&si, js, obs->streamName, NULL, &errCode); + if (s == NATS_OK) + { + js_lock(js); + s = natsConnection_JetStream(&(obs->pushJS), js->nc, &(js->opts)); + js_unlock(js); + } + if (s == NATS_OK) + *new_obs = obs; + else + _freeObs(obs); + + jsStreamInfo_Destroy(si); + + // If the stream was not found, return this error without updating the stack. + if ((s == NATS_NOT_FOUND) && (errCode == JSStreamNotFoundErr)) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +js_DeleteObjectStore(jsCtx *js, const char *bucket) +{ + natsStatus s = NATS_OK; + jsErrCode errCode = 0; + char *stream = NULL; + + if ((js == NULL) || nats_IsStringEmpty(bucket)) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (!nats_validBucketName(bucket)) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrInvalidStoreName); + + if (nats_asprintf(&stream, objNameTmpl, bucket) < 0) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + s = js_DeleteStream(js, stream, NULL, &errCode); + + NATS_FREE(stream); + + if ((s == NATS_NOT_FOUND) && (errCode == JSStreamNotFoundErr)) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStoreNamesList_Destroy(objStoreNamesList *list) +{ + int i; + + if (list == NULL) + return; + + for (i=0; iCount; i++) + NATS_FREE(list->List[i]); + NATS_FREE(list->List); + NATS_FREE(list); +} + +natsStatus +js_ObjectStoreNames(objStoreNamesList **list, jsCtx *js) +{ + natsStatus s = NATS_OK; + jsStreamNamesList *snl = NULL; + objStoreNamesList *osl = NULL; + int i = 0; + jsOptions opts; + + if ((list == NULL) || (js == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + jsOptions_Init(&opts); + opts.Stream.Info.SubjectsFilter = objAllChunksFilter; + s = js_StreamNames(&snl, js, &opts, NULL); + // Return early if there were no streams. + if (s == NATS_NOT_FOUND) + { + // We don't update the error stack for "not found" since we consider + // this a "normal" error. + return s; + } + osl = (objStoreNamesList*) NATS_CALLOC(1, sizeof(objStoreNamesList)); + if (osl == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + // We will allocate based on snl->Count, which may be more than needed + // if some stream names don't start with "OBJ_". + osl->List = (char**) NATS_CALLOC(snl->Count, sizeof(char*)); + if (osl->List == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + for (i=0; (s == NATS_OK) && (iCount); i++) + { + // Exclude the ones that don't start with "OBJ_". + if (strstr(snl->List[i], objNamePrefix) != snl->List[i]) + continue; + // Remove the "OBJ_" prefix. + DUP_STRING(s, osl->List[osl->Count], snl->List[i] + 4); + if (s == NATS_OK) + osl->Count++; + } + // Don't need that anymore. + jsStreamNamesList_Destroy(snl); + // If success, but we did not actually add any name (because none started + // with "OBJ_", switch state to NATS_NOT_FOUND. + if ((s == NATS_OK) && (osl->Count == 0)) + s = NATS_NOT_FOUND; + + if (s == NATS_OK) + *list = osl; + else + { + objStoreNamesList_Destroy(osl); + // If not found, return error without updating error stack. + if (s == NATS_NOT_FOUND) + return s; + } + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStoreStatus_Destroy(objStoreStatus *status) +{ + if (status == NULL) + return; + + NATS_FREE((char*) status->Bucket); + jsStreamInfo_Destroy(status->StreamInfo); + NATS_FREE(status); +} + +void +objStoreStatusesList_Destroy(objStoreStatusesList *list) +{ + int i; + + if (list == NULL) + return; + + for (i=0; iCount; i++) + objStoreStatus_Destroy(list->List[i]); + NATS_FREE(list->List); + NATS_FREE(list); +} + +static natsStatus +_createObjStoreStatus(objStoreStatus **new_oss, jsStreamInfo *info) +{ + natsStatus s = NATS_OK; + objStoreStatus *oss = NULL; + jsStreamConfig *cfg = info->Config; // Verified to be not NULL. + + oss = (objStoreStatus*) NATS_CALLOC(1, sizeof(objStoreStatus)); + if (oss == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + // We remove the prefix (we know it contains it) + DUP_STRING(s, oss->Bucket, cfg->Name + 4); + if (s != NATS_OK) + { + NATS_FREE(oss); + return NATS_UPDATE_ERR_STACK(s); + } + oss->Description = cfg->Description; + oss->TTL = NATS_NANOS_TO_MILLIS(cfg->MaxAge); + oss->Storage = cfg->Storage; + oss->Replicas = (int) cfg->Replicas; + oss->Size = info->State.Bytes; + oss->BackingStore = objBackingStore; + oss->Metadata.List = cfg->Metadata.List; + oss->Metadata.Count = cfg->Metadata.Count; + oss->StreamInfo = info; + oss->IsCompressed = cfg->Compression != js_StorageCompressionNone; + *new_oss = oss; + return NATS_OK; +} + +natsStatus +js_ObjectStoreStatuses(objStoreStatusesList **list, jsCtx *js) +{ + natsStatus s = NATS_OK; + jsStreamInfoList *sil = NULL; + objStoreStatusesList *osl = NULL; + jsOptions opts; + int i; + + if ((list == NULL) || (js == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + jsOptions_Init(&opts); + opts.Stream.Info.SubjectsFilter = objAllChunksFilter; + s = js_Streams(&sil, js, &opts, NULL); + // Return early if there were no streams. + if (s == NATS_NOT_FOUND) + { + // We don't update the error stack for "not found" since we consider + // this a "normal" error. + return s; + } + osl = (objStoreStatusesList*) NATS_CALLOC(1, sizeof(objStoreStatusesList)); + if (osl == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + // We will allocate based on sil->Count, which may be more than needed + // if some stream names don't start with "OBJ_". + osl->List = (objStoreStatus**) NATS_CALLOC(sil->Count, sizeof(char*)); + if (osl->List == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + for (i=0; (s == NATS_OK) && (iCount); i++) + { + jsStreamInfo *info = sil->List[i]; + + if (info->Config == NULL) + continue; + + // Exclude the ones that don't start with "OBJ_". + if (strstr(info->Config->Name, objNamePrefix) != info->Config->Name) + continue; + + s = _createObjStoreStatus(&(osl->List[osl->Count]), info); + // On success remove the stream info from the list so that + // we don't destroy it on cleanup on exit. Do not decrement + // sil->Count since there may be gaps in the list if we happen + // to have some info with name that do not start with "OBJ_". + if (s == NATS_OK) + { + osl->Count++; + sil->List[i] = NULL; + } + } + // Don't need that anymore. + jsStreamInfoList_Destroy(sil); + // If success, but we did not actually add any name (because none started + // with "OBJ_", switch state to NATS_NOT_FOUND. + if ((s == NATS_OK) && (osl->Count == 0)) + s = NATS_NOT_FOUND; + + if (s == NATS_OK) + *list = osl; + else + { + objStoreStatusesList_Destroy(osl); + // If not found, return error without updating error stack. + if (s == NATS_NOT_FOUND) + return s; + } + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStore_Destroy(objStore *obs) +{ + _releaseObs(obs); +} + +////////////////////////////////////////////////////////////////////////////// +// objStoreMeta APIs +////////////////////////////////////////////////////////////////////////////// + + +natsStatus +objStoreMeta_Init(objStoreMeta *meta) +{ + if (meta == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + memset(meta, 0, sizeof(objStoreMeta)); + return NATS_OK; +} + +static void +_destroyObjStoreLink(objStoreLink *link) +{ + if (link == NULL) + return; + + NATS_FREE((char*) link->Bucket); + NATS_FREE((char*) link->Name); + NATS_FREE(link); +} + +static void +_destroyObjStoreMeta(objStoreMeta *meta, bool freeObj) +{ + int i; + + if (meta == NULL) + return; + + NATS_FREE((char*) meta->Name); + NATS_FREE((char*) meta->Description); + natsHeader_Destroy(meta->Headers); + for (i=0; i<2*meta->Metadata.Count; i++) + NATS_FREE((char*) meta->Metadata.List[i]); + NATS_FREE((char*) meta->Metadata.List); + _destroyObjStoreLink(meta->Opts.Link); + if (freeObj) + NATS_FREE(meta); +} + +static natsStatus +_createObjStoreLink(objStoreLink **new_link) +{ + natsStatus s = NATS_OK; + objStoreLink *link = NULL; + + link = (objStoreLink*) NATS_CALLOC(1, sizeof(objStoreLink)); + if (link == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + *new_link = link; + else + _destroyObjStoreLink(link); + return NATS_UPDATE_ERR_STACK(s); +} + +// Will make a deep copy of `meta` into `clone` structure. +// The `clone` structure is already memset to '0's. +static natsStatus +_objStoreMeta_cloneInto(objStoreMeta *clone, objStoreMeta *meta) +{ + natsStatus s = NATS_OK; + + DUP_STRING(s, clone->Name, meta->Name); + IF_OK_DUP_STRING(s, clone->Description, meta->Description); + IFOK(s, nats_cloneMetadata(&clone->Metadata, &(meta->Metadata))); + if (s == NATS_OK) + { + clone->Opts.ChunkSize = meta->Opts.ChunkSize; + if (meta->Opts.Link != NULL) + { + s = _createObjStoreLink(&(clone->Opts.Link)); + if ((s == NATS_OK) && !nats_IsStringEmpty(meta->Opts.Link->Bucket)) + DUP_STRING(s, clone->Opts.Link->Bucket, meta->Opts.Link->Bucket); + if ((s == NATS_OK) && !nats_IsStringEmpty(meta->Opts.Link->Name)) + DUP_STRING(s, clone->Opts.Link->Name, meta->Opts.Link->Name); + } + } + if (s != NATS_OK) + _destroyObjStoreMeta(clone, false); + + return NATS_UPDATE_ERR_STACK(s); +} + +////////////////////////////////////////////////////////////////////////////// +// objStore management APIs +////////////////////////////////////////////////////////////////////////////// + +natsStatus +objStoreOptions_Init(objStoreOptions *opts) +{ + if (opts == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + memset(opts, 0, sizeof(objStoreOptions)); + return NATS_OK; +} + +void +objStoreInfo_Destroy(objStoreInfo *info) +{ + if (info == NULL) + return; + + _destroyObjStoreMeta(&(info->Meta), false); + NATS_FREE((char*) info->Bucket); + NATS_FREE((char*) info->NUID); + NATS_FREE((char*) info->Digest); + NATS_FREE(info); +} + +static natsStatus +_encodeName(char **en, const char *name) +{ + natsStatus s = nats_Base64RawURL_EncodeString((const unsigned char*) name, (int) strlen(name), en); + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_unmarshalObjStoreLink(nats_JSON *json, const char *fieldName, objStoreLink **new_link) +{ + natsStatus s = NATS_OK; + nats_JSON *obj = NULL; + objStoreLink *link = NULL; + + s = nats_JSONGetObject(json, fieldName, &obj); + if ((s == NATS_OK) && (obj == NULL)) + return NATS_OK; + + link = (objStoreLink*) NATS_CALLOC(1, sizeof(objStoreLink)); + if (link == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + IFOK(s, nats_JSONGetStr(obj, "bucket", (char**) &(link->Bucket))); + IFOK(s, nats_JSONGetStr(obj, "name", (char**) &(link->Name))); + + if (s == NATS_OK) + *new_link = link; + else + _destroyObjStoreLink(link); + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_unmarshalObjStoreInfoMetaOptions(nats_JSON *json, const char *fieldName, objStoreMetaOptions *opts) +{ + natsStatus s = NATS_OK; + nats_JSON *obj = NULL; + + s = nats_JSONGetObject(json, fieldName, &obj); + if ((s == NATS_OK) && (obj == NULL)) + return NATS_OK; + + s = _unmarshalObjStoreLink(obj, "link", &(opts->Link)); + IFOK(s, nats_JSONGetUInt32(obj, "max_chunk_size", &(opts->ChunkSize))); + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_unmarshalObjStoreInfo(objStoreInfo **new_info, const char *data, int dataLen) +{ + natsStatus s = NATS_OK; + nats_JSON *json = NULL; + objStoreInfo *info = (objStoreInfo*) NATS_CALLOC(1, sizeof(objStoreInfo)); + + if (info == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + s = nats_JSONParse(&json, data, dataLen); + IFOK(s, nats_JSONGetStr(json, "name", (char**) &(info->Meta.Name))); + IFOK(s, nats_JSONGetStr(json, "description", (char**) &(info->Meta.Description))); + IFOK(s, nats_unmarshalHeader(json, "headers", &(info->Meta.Headers))); + IFOK(s, nats_unmarshalMetadata(json, "metadata", &(info->Meta.Metadata))); + IFOK(s, _unmarshalObjStoreInfoMetaOptions(json, "options", &(info->Meta.Opts))); + IFOK(s, nats_JSONGetStr(json, "bucket", (char**) &(info->Bucket))); + IFOK(s, nats_JSONGetStr(json, "nuid", (char**) &(info->NUID))); + IFOK(s, nats_JSONGetULong(json, "size", &(info->Size))); + IFOK(s, nats_JSONGetTime(json, "mtime", &(info->ModTime))); + IFOK(s, nats_JSONGetUInt32(json, "chunks", &(info->Chunks))); + IFOK(s, nats_JSONGetStr(json, "digest", (char**) &(info->Digest))); + IFOK(s, nats_JSONGetBool(json, "deleted", &(info->Deleted))); + + nats_JSONDestroy(json); + + if (s == NATS_OK) + *new_info = info; + else + objStoreInfo_Destroy(info); + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_marshalObjStoreInfo(natsBuffer *buf, objStoreInfo *info) +{ + natsStatus s; + + s = natsBuf_AppendByte(buf, '{'); + IFOK(s, nats_marshalString(buf, false, false, "name", info->Meta.Name)); + IFOK(s, nats_marshalString(buf, true, true, "description", info->Meta.Description)); + IFOK(s, nats_marshalHeader(buf, true, true, "headers", info->Meta.Headers)); + IFOK(s, nats_marshalMetadata(buf, true, "metadata", &(info->Meta.Metadata))); + IFOK(s, natsBuf_Append(buf, ",\"options\":{", -1)); + if (s == NATS_OK) + { + bool comma = false; + + if (info->Meta.Opts.ChunkSize > 0) + { + comma = true; + s = nats_marshalULong(buf, false, "max_chunk_size", (uint64_t) info->Meta.Opts.ChunkSize); + } + if ((s == NATS_OK) && (info->Meta.Opts.Link != NULL)) + { + if (comma) + s = natsBuf_AppendByte(buf, ','); + IFOK(s, natsBuf_Append(buf, "\"link\":{", -1)); + IFOK(s, nats_marshalString(buf, false, false, "bucket", info->Meta.Opts.Link->Bucket)); + IFOK(s, nats_marshalString(buf, true, true, "name", info->Meta.Opts.Link->Name)); + IFOK(s, natsBuf_AppendByte(buf, '}')); + } + } + IFOK(s, natsBuf_AppendByte(buf, '}')); + IFOK(s, nats_marshalString(buf, false, true, "bucket", info->Bucket)); + IFOK(s, nats_marshalString(buf, false, true, "nuid", info->NUID)); + IFOK(s, nats_marshalULong(buf, true, "size", info->Size)); + IFOK(s, nats_marshalTimeUTC(buf, true, "mtime", info->ModTime)); + IFOK(s, nats_marshalULong(buf, true, "chunks", (uint64_t) info->Chunks)); + IFOK(s, nats_marshalString(buf, true, true, "digest", info->Digest)); + if ((s == NATS_OK) && info->Deleted) + { + s = natsBuf_Append(buf, ",\"deleted\":true", -1); + } + IFOK(s, natsBuf_AppendByte(buf, '}')); + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_checkElapsed(int64_t *elapsed, int64_t start, int64_t timeout) +{ + *elapsed = nats_Now() - start; + if (*elapsed > timeout) + return nats_setDefaultError(NATS_TIMEOUT); + return NATS_OK; +} + +static natsStatus +_getInfo(objStoreInfo **new_info, objStore *obs, int64_t start, int64_t timeout, const char *name, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + objStoreInfo *info = NULL; + bool showDeleted = (opts != NULL ? opts->ShowDeleted : false); + char *metaSubj = NULL; + char *encName = NULL; + int64_t elapsed = 0; + + if (new_info == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (nats_IsStringEmpty(name)) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrNameIsRequired); + + s = _encodeName(&encName, name); + if ((s == NATS_OK) && (nats_asprintf(&metaSubj, objMetaPreTmpl, obs->name, encName) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + natsMsg *msg = NULL; + jsOptions jo; + jsDirectGetMsgOptions dgmo; + + jsOptions_Init(&jo); + s = _checkElapsed(&elapsed, start, timeout); + if (s == NATS_OK) + { + jo.Wait = timeout-elapsed; + + jsDirectGetMsgOptions_Init(&dgmo); + dgmo.LastBySubject = metaSubj; + } + IFOK(s, js_DirectGetMsg(&msg, obs->js, obs->streamName, &jo, &dgmo)); + if (s == NATS_OK) + { + s = _unmarshalObjStoreInfo(&info, natsMsg_GetData(msg), natsMsg_GetDataLength(msg)); + if (s != NATS_OK) + s = nats_setError(s, "%s", obsErrBadObjectMeta); + } + if (s == NATS_OK) + info->ModTime = natsMsg_GetTime(msg); + natsMsg_Destroy(msg); + } + if ((s == NATS_OK) && !showDeleted && info->Deleted) + s = NATS_NOT_FOUND; + + NATS_FREE(encName); + NATS_FREE(metaSubj); + + if (s == NATS_OK) + *new_info = info; + else + { + objStoreInfo_Destroy(info); + if (s == NATS_NOT_FOUND) + { + nats_clearLastError(); + return s; + } + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_GetInfo(objStoreInfo **new_info, objStore *obs, const char *name, objStoreOptions *opts) +{ + natsStatus s; + int64_t start = 0; + int64_t timeout = 0; + + if (obs == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + start = nats_Now(); + s = _getInfo(new_info, obs, start, timeout, name, opts); + if (s == NATS_NOT_FOUND) + return s; + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_publishMeta(jsCtx *js, objStoreInfo *info, int64_t timeout) +{ + natsStatus s = NATS_OK;; + char *subj = NULL; + char *en = NULL; + natsMsg *mm = NULL; + natsBuffer buf; + + s = natsBuf_Init(&buf, 256); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + + // marshal the object into json, don't store an actual time + info->ModTime = 0; + s = _marshalObjStoreInfo(&buf, info); + IFOK(s, _encodeName(&en, info->Meta.Name)); + if ((s == NATS_OK) && (nats_asprintf(&subj, objMetaPreTmpl, info->Bucket, en) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + IFOK(s, natsMsg_Create(&mm, subj, NULL, natsBuf_Data(&buf), natsBuf_Len(&buf))); + IFOK(s, natsMsgHeader_Set(mm, JSMsgRollup, JSMsgRollupSubject)); + if (s == NATS_OK) + { + jsPubOptions po; + + jsPubOptions_Init(&po); + po.MaxWait = timeout; + js_PublishMsg(NULL, js, mm, &po, NULL); + } + + natsMsg_Destroy(mm); + NATS_FREE(subj); + NATS_FREE(en); + natsBuf_Cleanup(&buf); + + // set the ModTime in case it's returned to the user, even though it's not the correct time. + info->ModTime = nats_NowInNanoSeconds(); + + return NATS_UPDATE_ERR_STACK(s); +} + + +natsStatus +objStore_UpdateMeta(objStore *obs, const char *name, objStoreMeta *meta) +{ + natsStatus s = NATS_OK; + objStoreInfo *info = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + + if ((obs == NULL) || (meta == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + // Grab the current meta. + s = _getInfo(&info, obs, start, timeout, name, NULL); + if (s != NATS_OK) + { + if (s == NATS_NOT_FOUND) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrUpdateMetaDelete); + + return NATS_UPDATE_ERR_STACK(s); + } + s = _checkElapsed(&elapsed, start, timeout); + if (s == NATS_OK) + { + // If the new name is different from the old, and it exists, error + // If there was an error that was not NATS_NOT_FOUND, error. + if (strcmp(name, meta->Name) != 0) + { + objStoreInfo *existingInfo = NULL; + objStoreOptions so; + + objStoreOptions_Init(&so); + so.ShowDeleted = true; + + s = _getInfo(&existingInfo, obs, start, timeout, meta->Name, &so); + if (s == NATS_NOT_FOUND) + s = NATS_OK; + else if ((s == NATS_OK) && (!existingInfo->Deleted)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrObjectAlreadyExists); + + objStoreInfo_Destroy(existingInfo); + } + } + if (s == NATS_OK) + { + const char *orgName = info->Meta.Name; + const char *orgDesc = info->Meta.Description; + natsHeader *orgHdr = info->Meta.Headers; + natsMetadata orgMD = info->Meta.Metadata; + + info->Meta.Name = meta->Name; + info->Meta.Description = meta->Description; + info->Meta.Headers = meta->Headers; + info->Meta.Metadata = meta->Metadata; + + s = _checkElapsed(&elapsed, start, timeout); + IFOK(s, _publishMeta(obs->js, info, timeout-elapsed)); + + // Retore original values so that we don't free user's provided data + // from the `meta` parameter. + info->Meta.Name = orgName; + info->Meta.Description = orgDesc; + info->Meta.Headers = orgHdr; + info->Meta.Metadata = orgMD; + } + if (s == NATS_OK) + { + // did the name of this object change? We just stored the meta under the new name + // so delete the meta from the old name via purge stream for subject. + if (strcmp(name, meta->Name) != 0) + { + char *metaSubj = NULL; + char *en = NULL; + + s = _encodeName(&en, name); + if ((s == NATS_OK) && (nats_asprintf(&metaSubj, objMetaPreTmpl, obs->name, en) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + IFOK(s, _checkElapsed(&elapsed, start, timeout)) + if (s == NATS_OK) + { + jsOptions jo; + + jsOptions_Init(&jo); + jo.Wait = timeout-elapsed; + jo.Stream.Purge.Subject = metaSubj; + s = js_PurgeStream(obs->js, obs->streamName, &jo, NULL); + } + NATS_FREE(metaSubj); + NATS_FREE(en); + } + } + objStoreInfo_Destroy(info); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_Delete(objStore *obs, const char *name) +{ + natsStatus s; + objStoreInfo *info = NULL; + char *chunkSubj = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + + if (obs == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + s = _getInfo(&info, obs, start, timeout, name, NULL); + if (s != NATS_OK) + { + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); + } + if (nats_IsStringEmpty(info->NUID)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrBadObjectMeta); + IFOK(s, _checkElapsed(&elapsed, start, timeout)); + if (s == NATS_OK) + { + const char *orgDigest = info->Digest; + + // Place a rollup delete marker and publis the info. + info->Deleted = true; + info->Size = 0; + info->Chunks = 0; + info->Digest = NULL; + s = _publishMeta(obs->js, info, timeout-elapsed); + + // Restore original digest for proper cleanup. + info->Digest = orgDigest; + } + if ((s == NATS_OK) && (nats_asprintf(&chunkSubj, objChunksPreTmpl, obs->name, info->NUID) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + IFOK(s, _checkElapsed(&elapsed, start, timeout)); + if (s == NATS_OK) + { + jsOptions jo; + + jsOptions_Init(&jo); + jo.Stream.Purge.Subject = chunkSubj; + jo.Wait = timeout-elapsed; + s = js_PurgeStream(obs->js, obs->streamName, &jo, NULL); + } + + objStoreInfo_Destroy(info); + NATS_FREE(chunkSubj); + + return NATS_UPDATE_ERR_STACK(s); +} + +static bool +_isLink(objStoreInfo *info) +{ + return info->Meta.Opts.Link != NULL; +} + +static natsStatus +_addLink(objStoreInfo **new_info, objStore *obs, const char *name, const char *objBucket, const char *objName) +{ + natsStatus s = NATS_OK; + objStoreInfo *einfo = NULL; + objStoreInfo *info = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + char nuid[NUID_BUFFER_LEN+1]; + objStoreOptions o; + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + // If object with link's name is found, error. + // If link with link's name is found, that's okay to overwrite. + // If there was an error that was not NATS_NOT_FOUND, error. + objStoreOptions_Init(&o); + o.ShowDeleted = true; + s = _getInfo(&einfo, obs, start, timeout, name, &o); + if ((s == NATS_OK) && !_isLink(einfo)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrObjectAlreadyExists); + else if (s == NATS_NOT_FOUND) + s = NATS_OK; + + // create the info/meta for the link + if (s == NATS_OK) + { + info = (objStoreInfo*) NATS_CALLOC(1, sizeof(objStoreInfo)); + if (info == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + { + info->ModTime = nats_NowInNanoSeconds(); + info->Meta.Opts.Link = (objStoreLink*) NATS_CALLOC(1, sizeof(objStoreLink)); + if (info->Meta.Opts.Link == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + } + IF_OK_DUP_STRING(s, info->Meta.Name, name); + IF_OK_DUP_STRING(s, info->Meta.Opts.Link->Bucket, objBucket); + if ((s == NATS_OK) && !nats_IsStringEmpty(objName)) + DUP_STRING(s, info->Meta.Opts.Link->Name, objName); + IF_OK_DUP_STRING(s, info->Bucket, obs->name); + IFOK(s, natsNUID_Next(nuid, sizeof(nuid))); + IF_OK_DUP_STRING(s, info->NUID, nuid); + IFOK(s, _checkElapsed(&elapsed, start, timeout)); + + // Put the link object + IFOK(s, _publishMeta(obs->js, info, timeout-elapsed)); + + // Destroy existing info object. + objStoreInfo_Destroy(einfo); + + // IF ok and user wants the info back, return it. + if ((s == NATS_OK) && (new_info != NULL)) + { + *new_info = info; + return NATS_OK; + } + + // User did not want or there was an error, destroy it. + objStoreInfo_Destroy(info); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_AddLink(objStoreInfo **new_info, objStore *obs, const char *name, objStoreInfo *obj) +{ + natsStatus s; + + if ((obs == NULL) || (obj == NULL) || nats_IsStringEmpty(obj->Meta.Name)) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (obj->Deleted) + return nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrNoLinkToDeleted); + if (_isLink(obj)) + return nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrNoLinkToLink); + + s = _addLink(new_info, obs, name, obj->Bucket, obj->Meta.Name); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_AddBucketLink(objStoreInfo **new_info, objStore *obs, const char *name, objStore *bucket) +{ + natsStatus s; + + if ((obs == NULL) || (bucket == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + s = _addLink(new_info, obs, name, bucket->name, NULL); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_Seal(objStore *obs) +{ + natsStatus s; + jsStreamInfo *si = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + + if (obs == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + s = js_GetStreamInfo(&si, obs->js, obs->streamName, NULL, NULL); + IFOK(s, _checkElapsed(&elapsed, start, timeout)); + if (s == NATS_OK) + { + jsOptions jo; + + jsOptions_Init(&jo); + jo.Wait = timeout-elapsed; + + si->Config->Sealed = true; + s = js_UpdateStream(NULL, obs->js, si->Config, &jo, NULL); + } + + jsStreamInfo_Destroy(si); + + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +static void +_freeWatcher(objStoreWatcher *w) +{ + objStore *obs = w->obs; + natsSubscription_Destroy(w->sub); + natsMutex_Destroy(w->mu); + NATS_FREE(w); + _releaseObs(obs); +} + +static void +_releaseWatcher(objStoreWatcher *w) +{ + bool doFree; + + if (w == NULL) + return; + + natsMutex_Lock(w->mu); + doFree = (--(w->refs) == 0); + natsMutex_Unlock(w->mu); + + if (doFree) + _freeWatcher(w); +} + +natsStatus +objStoreWatcher_Stop(objStoreWatcher *w) +{ + natsStatus s = NATS_OK; + + if (w == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + natsMutex_Lock(w->mu); + if (!w->stopped) + { + w->stopped = true; + s = natsSubscription_Unsubscribe(w->sub); + } + natsMutex_Unlock(w->mu); + + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStoreWatcher_Destroy(objStoreWatcher *w) +{ + objStoreWatcher_Stop(w); + _releaseWatcher(w); +} + +natsStatus +objStoreWatchOptions_Init(objStoreWatchOptions *opts) +{ + if (opts == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + memset(opts, 0, sizeof(objStoreWatchOptions)); + return NATS_OK; +} + +natsStatus +objStore_Watch(objStoreWatcher **new_watcher, objStore *obs, objStoreWatchOptions *opts) +{ + natsStatus s = NATS_OK; + objStoreWatcher *w = NULL; + char *allMeta = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + objStoreWatchOptions *o = NULL; + objStoreWatchOptions lo; + + if ((new_watcher == NULL) || (obs == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + objStoreWatchOptions_Init(&lo); + o = (opts == NULL ? &lo : opts); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + w = (objStoreWatcher*) NATS_CALLOC(1, sizeof(objStoreWatcher)); + if (w == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + _retainObs(obs); + w->obs = obs; + w->refs = 1; + w->ignoreDel = o->IgnoreDeletes; + s = natsMutex_Create(&w->mu); + if ((s == NATS_OK) && (nats_asprintf(&allMeta, objAllMetaPreTmpl, obs->name) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + if (s == NATS_OK) + { + jsErrCode jErr = 0; + natsMsg *msg = NULL; + + // if there are no messages on the stream and we are not watching + // updates only, we will send the marker. + s = js_GetLastMsg(&msg, obs->js, obs->streamName, allMeta, NULL, &jErr); + if (!o->UpdatesOnly) + { + if (s == NATS_NOT_FOUND) + { + nats_clearLastError(); + s = NATS_OK; + w->initDone = true; + w->retMarker = true; + } + } + else if (s == NATS_OK) + { + w->initDone = true; + } + natsMsg_Destroy(msg); + } + IFOK(s, _checkElapsed(&elapsed, start, timeout)) + { + jsOptions jo; + jsSubOptions so; + + jsOptions_Init(&jo); + jo.Wait = timeout-elapsed; + + // Used ordered consumer to deliver results. + jsSubOptions_Init(&so); + so.Ordered = true; + so.Stream = obs->streamName; + if (o->UpdatesOnly) + so.Config.DeliverPolicy = js_DeliverNew; + s = js_SubscribeSync(&(w->sub), obs->js, allMeta, &jo, &so, NULL); + } + if (s == NATS_OK) + *new_watcher = w; + else + objStoreWatcher_Destroy(w); + + NATS_FREE(allMeta); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStoreWatcher_Next(objStoreInfo **new_info, objStoreWatcher *w, int64_t timeout) +{ + natsStatus s = NATS_OK; + objStoreInfo *info = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + + if ((new_info == NULL) || (w == NULL) || (timeout <= 0)) + return nats_setDefaultError(NATS_INVALID_ARG); + + *new_info = NULL; + + natsMutex_Lock(w->mu); +GET_NEXT: + if (w->stopped) + { + s = nats_setDefaultError(NATS_ILLEGAL_STATE); + } + if (w->retMarker) + { + // We return a NULL info and switch this OFF so we don't get back here. + w->retMarker = false; + } + else + { + natsMsg *msg = NULL; + int64_t tm = 0; + uint64_t pending = 0; + bool next = false; + + // Get the next message. + w->refs++; + natsMutex_Unlock(w->mu); + + s = natsSubscription_NextMsg(&msg, w->sub, timeout-elapsed); + + natsMutex_Lock(w->mu); + if (w->stopped) + { + natsMutex_Unlock(w->mu); + _releaseWatcher(w); + return NATS_ILLEGAL_STATE; + } + w->refs--; + IFOK(s, _unmarshalObjStoreInfo(&info, natsMsg_GetData(msg), natsMsg_GetDataLength(msg))); + IFOK(s, js_getMetaData(natsMsg_GetReply(msg), NULL, NULL, NULL, NULL, NULL, NULL, &tm, &pending, 2)); + if (s == NATS_OK) + { + if (!w->ignoreDel || !info->Deleted) + { + info->ModTime = tm; + } + else + { + objStoreInfo_Destroy(info); + info = NULL; + // Check remaining time. + s = _checkElapsed(&elapsed, start, timeout); + if (s == NATS_OK) + next = true; + } + if (!w->initDone && pending == 0) + { + w->initDone = true; + w->retMarker = true; + } + } + natsMsg_Destroy(msg); + + if (next) + goto GET_NEXT; + } + natsMutex_Unlock(w->mu); + + if (s == NATS_OK) + *new_info = info; + + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStoreInfoList_Destroy(objStoreInfoList *list) +{ + int i; + + if (list == NULL) + return; + + for (i=0; iCount; i++) + objStoreInfo_Destroy(list->List[i]); + NATS_FREE(list->List); + NATS_FREE(list); +} + +int obsInitialListCap = obsInitialListCapValue; + +natsStatus +objStore_List(objStoreInfoList **new_list, objStore *obs, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + objStoreInfoList *list = NULL; + objStoreWatcher *w = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + int count = 0; + int cap = obsInitialListCap; + objStoreWatchOptions o; + + if ((new_list == NULL) || (obs == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + objStoreWatchOptions_Init(&o); + // If not option to ask for "ShowDeleted", then we ignore them. + if ((opts == NULL) || !opts->ShowDeleted) + o.IgnoreDeletes = true; + + s = objStore_Watch(&w, obs, &o); + while ((s == NATS_OK) && ((s = _checkElapsed(&elapsed, start, timeout)) == NATS_OK)) + { + objStoreInfo *info = NULL; + + s = objStoreWatcher_Next(&info, w, timeout-elapsed); + if ((s == NATS_OK) && (info == NULL)) + { + break; + } + else if (s == NATS_OK) + { + if (list == NULL) + { + list = (objStoreInfoList*) NATS_CALLOC(1, sizeof(objStoreInfo)); + if (list == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + if ((s == NATS_OK) && ((list->List == NULL) || (count == cap))) + { + if (list->List != NULL) + cap *= 2; + + list->List = (objStoreInfo**) NATS_REALLOC(list->List, cap * sizeof(objStoreInfo*)); + if (list->List == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + { + int i; + for (i=count; iList[i] = NULL; + } + } + if (s == NATS_OK) + { + list->List[count++] = info; + list->Count++; + } + } + } + // This will also take care of stopping it. + objStoreWatcher_Destroy(w); + + if (s == NATS_OK) + { + if (count == 0) + { + objStoreInfoList_Destroy(list); + return NATS_NOT_FOUND; + } + *new_list = list; + } + else + objStoreInfoList_Destroy(list); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_Status(objStoreStatus **new_sts, objStore *obs) +{ + natsStatus s = NATS_OK; + jsStreamInfo *si = NULL; + + if ((new_sts == NULL) || (obs == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + s = js_GetStreamInfo(&si, obs->js, obs->streamName, NULL, NULL); + IFOK(s, _createObjStoreStatus(new_sts, si)); + + if (s != NATS_OK) + { + jsStreamInfo_Destroy(si); + if (s == NATS_NOT_FOUND) + return s; + } + + return NATS_UPDATE_ERR_STACK(s); +} + +////////////////////////////////////////////////////////////////////////////// +// objStore PUT APIs +////////////////////////////////////////////////////////////////////////////// + +static void +_freePut(objStorePut *put) +{ + objStoreInfo_Destroy(put->info); + NATS_FREE(put->metaSubj); + NATS_FREE(put->echunkSubj); + NATS_FREE(put->chunkSubj); + NATS_FREE(put->errTxt); + nats_hashDestroy(put->h); + _releaseObs(put->obs); + natsMutex_Destroy(put->mu); + NATS_FREE(put); +} + +static void +_setPutErr(objStorePut *put, natsStatus err, char *errTxt) +{ + natsMutex_Lock(put->mu); + if (put->err == NATS_OK) + { + put->err = err; + put->errTxt = errTxt; + } + else + NATS_FREE(errTxt); + natsMutex_Unlock(put->mu); +} + +static natsStatus +_getPutErr(objStorePut *put) +{ + natsStatus s = NATS_OK; + + natsMutex_Lock(put->mu); + if (put->err != NATS_OK) + { + if (nats_IsStringEmpty(put->errTxt)) + s = nats_setDefaultError(put->err); + else + s = nats_setError(put->err, "%s", put->errTxt); + } + natsMutex_Unlock(put->mu); + return s; +} + +static void +_putErrHandler(jsCtx *js, jsPubAckErr *pae, void *closure) +{ + objStorePut *put = (objStorePut*) closure; + char *errTxt = NULL; + + if ((!nats_IsStringEmpty(pae->ErrText)) && + (nats_asprintf(&errTxt, "%s (%d)", pae->ErrText, pae->ErrCode) < 0)) + { + // Nothing we can do. We can't use static error text here + // because errTxt would be freed later. + errTxt = NULL; + } + _setPutErr(put, pae->Err, errTxt); +} + +static void +_retainPut(objStorePut *put) +{ + natsMutex_Lock(put->mu); + put->refs++; + natsMutex_Unlock(put->mu); +} + +static void +_releasePut(objStorePut *put) +{ + int refs; + + natsMutex_Lock(put->mu); + refs = --(put->refs); + natsMutex_Unlock(put->mu); + if (refs == 0) + _freePut(put); +} + +static natsStatus +_getDigestValue(char **digest, nats_hash *h) +{ + natsStatus s = NATS_OK; + char *d = NULL; + unsigned int len = 0; + unsigned char val[NATS_HASH_MAX_LEN]; + + s = nats_hashSum(h, val, &len); + IFOK(s, nats_Base64URL_EncodeString((const unsigned char*) val, (int) len, &d)); + if ((s == NATS_OK) && (nats_asprintf(digest, objDigestTmpl, d)) < 0) + s = nats_setDefaultError(NATS_NO_MEMORY); + NATS_FREE(d); + return NATS_UPDATE_ERR_STACK(s); +} + +static void +_purgeChunks(objStorePut *put, int64_t timeout) +{ + jsOptions opts; + jsPubOptions po; + int64_t start; + int64_t remaining; + + jsOptions_Init(&opts); + jsPubOptions_Init(&po); + + // We will not udpate the error stack for those calls. + nats_doNotUpdateErrStack(true); + // Keep track of when we start. + start = nats_Now(); + // Use the full timeout to wait for pub async to complete. + po.MaxWait = timeout; + js_PublishAsyncComplete(put->pubJS, &po); + + // Set the purge subject + opts.Stream.Purge.Subject = put->chunkSubj; + // Compute the remaining time, but use a minimum for the purge call. + remaining = timeout - (nats_Now() - start); + if (remaining < 1000) + remaining = 1000; + opts.Wait = remaining; + js_PurgeStream(put->obs->js, put->obs->streamName, &opts, NULL); + nats_doNotUpdateErrStack(false); +} + +natsStatus +objStorePut_Add(objStorePut *put, const void *data, int dataLen) +{ + natsStatus s = NATS_OK; + int remaining = dataLen; + int off = 0; + int chunkSize = 0; + + if ((put == NULL) || ((data == NULL) && (dataLen > 0)) || + ((data != NULL) && (dataLen == 0)) || (dataLen < 0)) + { + return nats_setDefaultError(NATS_INVALID_ARG); + } + + s = _getPutErr(put); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + + if (dataLen == 0) + return NATS_OK; + + chunkSize = (int) put->info->Meta.Opts.ChunkSize; + + do { + int size = (remaining > chunkSize ? chunkSize : remaining); + const void *chd = (const void*) (((const char *)data)+off); + + // Indicate that we are doing a publish, so if there is error before + // completion, then we will need to purge the partial chunks. + put->pcof = true; + + // Update hash. + s = nats_hashWrite(put->h, chd, size); + + // Send the message itself. + IFOK(s, js_PublishAsync(put->pubJS, put->chunkSubj, chd, size, NULL)); + if (s == NATS_OK) + { + // Update totals. + put->sent++; + put->total += (uint64_t) size; + + // Check for async pub error. + s = _getPutErr(put); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + } + if (s == NATS_OK) + { + off += size; + remaining -= size; + } + } while ((s == NATS_OK) && (remaining > 0)); + + if (s != NATS_OK) + _setPutErr(put, s, NULL); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStorePut_Complete(objStoreInfo **new_info, objStorePut *put, int64_t timeout) +{ + natsStatus s = NATS_OK; + natsMsg *mm = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + natsBuffer buf; + + if ((put == NULL) || (timeout < 0)) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (timeout == 0) + { + js_lock(put->pubJS); + timeout = put->pubJS->opts.Wait; + js_unlock(put->pubJS); + } + + s = _getPutErr(put); + if (s != NATS_OK) + { + _purgeChunks(put, timeout); + return NATS_UPDATE_ERR_STACK(s); + } + + jsPubOptions po; + + jsPubOptions_Init(&po); + po.MaxWait = timeout; + + // Place meta info. + put->info->Size = put->total; + put->info->Chunks = put->sent; + + // Initialize the buffer. + s = natsBuf_Init(&buf, 512); + + // Compute the digest + IFOK(s, _getDigestValue((char **) &(put->info->Digest), put->h)); + + // Marshal the info object into buffer. + IFOK(s, _marshalObjStoreInfo(&buf, put->info)); + + // Create the meta message. + IFOK(s, natsMsg_Create(&mm, put->metaSubj, NULL, natsBuf_Data(&buf), natsBuf_Len(&buf))); + IFOK(s, natsMsgHeader_Set(mm, JSMsgRollup, JSMsgRollupSubject)); + IFOK(s, js_PublishMsgAsync(put->pubJS, &mm, &po)); + if (s == NATS_OK) + { + s = _checkElapsed(&elapsed, start, timeout); + if (s == NATS_OK) + po.MaxWait = timeout-elapsed; + // Wait for all to be processed. + IFOK(s, js_PublishAsyncComplete(put->pubJS, &po)); + // Check for async error + if (s == NATS_OK) + s = _getPutErr(put); + } + if (s == NATS_OK) + { + // This time is not actually the correct time. + put->info->ModTime = nats_NowInNanoSeconds(); + + // Delete any original chunks if needed. + if (put->echunkSubj != NULL) + { + jsOptions opts; + + jsOptions_Init(&opts); + opts.Stream.Purge.Subject = put->echunkSubj; + if (_checkElapsed(&elapsed, start, timeout) != NATS_OK) + { + // We have already expired, use a nominal low timeout. + opts.Wait = 1000; + } + else + opts.Wait = timeout-elapsed; + nats_doNotUpdateErrStack(true); + js_PurgeStream(put->obs->js, put->obs->streamName, &opts, NULL); + nats_doNotUpdateErrStack(false); + } + + // Return the info if requested (and dissociate from the put object). + if (new_info != NULL) + { + *new_info = put->info; + put->info = NULL; + } + put->pcof = false; + } + if (s != NATS_OK) + { + // If we failed (say waiting for pub async to complete), we could have + // already exhausted the timeout, so if that is the case, let's use + // a 1 second timeout to try purge partials. + elapsed = nats_Now() - start; + if (elapsed < timeout) + timeout = timeout-elapsed; + else + timeout = 1000; + + _purgeChunks(put, timeout); + } + + natsMsg_Destroy(mm); + natsBuf_Cleanup(&buf); + + return NATS_UPDATE_ERR_STACK(s); +} + +void +objStorePut_Destroy(objStorePut *put) +{ + if (put == NULL) + return; + + if (put->pcof) + _purgeChunks(put, jsDefaultRequestWait); + + jsCtx_Destroy(put->pubJS); + + _releasePut(put); +} + +static void +_onPutJSReleased(void *arg) +{ + objStorePut *put = (objStorePut*) arg; + + _releasePut(put); +} + +natsStatus +objStore_Put(objStorePut **new_put, objStore *obs, objStoreMeta *pMeta) +{ + natsStatus s = NATS_OK; + objStorePut *put = NULL; + objStoreMeta *meta = NULL; + objStoreInfo *einfo = NULL; + char *encMetaName= NULL; + char nuid[NUID_BUFFER_LEN+1]; + + if ((new_put == NULL) || (obs == NULL) || (pMeta == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + if (nats_IsStringEmpty(pMeta->Name)) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrBadObjectMeta); + + if (pMeta->Opts.Link != NULL) + return nats_setError(NATS_INVALID_ARG, "%s", obsErrLinkNotAllowed); + + put = (objStorePut*) NATS_CALLOC(1, sizeof(objStorePut)); + if (put == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + put->refs = 1; + put->obs = obs; + _retainObs(obs); + s = natsMutex_Create(&(put->mu)); + if (s == NATS_OK) + { + put->info = (objStoreInfo*) NATS_CALLOC(1, sizeof(objStoreInfo)); + if (put->info == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + // Clone the user provided meta into our info object. + if (s == NATS_OK) + { + // Point to the embedded Meta structure in the objStoreInfo structure, + meta = &(put->info->Meta); + // Make a deep copy into it. + s = _objStoreMeta_cloneInto(meta, pMeta); + if ((s == NATS_OK) && (meta->Opts.ChunkSize == 0)) + meta->Opts.ChunkSize = obsDefaultChunkSize; + } + // Set the info's bucket name based on our object store name. + IF_OK_DUP_STRING(s, put->info->Bucket, obs->name); + + // Create the new nuid so chunks go on a new subject if the name is re-used. + IFOK(s, natsNUID_Next(nuid, sizeof(nuid))); + IF_OK_DUP_STRING(s, put->info->NUID, nuid); + if ((s == NATS_OK) && (nats_asprintf(&(put->chunkSubj), objChunksPreTmpl, obs->name, nuid) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + + // Encode the meta name, which is going to be used for creating the meta subject. + IFOK(s, _encodeName(&encMetaName, meta->Name)); + if ((s == NATS_OK) && (nats_asprintf(&(put->metaSubj), objMetaPreTmpl, obs->name, encMetaName) < 0)) + s = nats_setDefaultError(NATS_NO_MEMORY); + + // Create our own JS context to handle errors etc. + if (s == NATS_OK) + { + jsOptions pubJSOpts; + + jsOptions_Init(&pubJSOpts); + pubJSOpts.PublishAsync.ErrHandler = _putErrHandler; + pubJSOpts.PublishAsync.ErrHandlerClosure = (void*) put; + + s = natsConnection_JetStream(&(put->pubJS), obs->js->nc, &pubJSOpts); + if (s == NATS_OK) + { + _retainPut(put); + js_setOnReleasedCb(put->pubJS, _onPutJSReleased, (void*) put); + } + } + + // Create hash for digest. + IFOK(s, nats_hashNew(&(put->h))); + + // Grab existing meta info (einfo). Ok to be found or not found, any other error is a problem. + if (s == NATS_OK) + { + objStoreOptions so; + + objStoreOptions_Init(&so); + so.ShowDeleted = true; + s = objStore_GetInfo(&einfo, obs, meta->Name, &so); + // If there is an existing info and it was not deleted, collect the chunk + // subject which will be the deciding factor to purge the old chunks when + // adding data iscomplete. + if ((s == NATS_OK) && (!einfo->Deleted)) + { + if (nats_asprintf(&(put->echunkSubj), objChunksPreTmpl, obs->name, einfo->NUID) < 0) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + else if (s == NATS_NOT_FOUND) + s = NATS_OK; + } + + if (s == NATS_OK) + *new_put = put; + else + objStorePut_Destroy(put); + + NATS_FREE(encMetaName); + objStoreInfo_Destroy(einfo); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_PutString(objStoreInfo **new_info, objStore *obs, const char *name, const char *data) +{ + natsStatus s = NATS_OK; + objStorePut *put = NULL; + int len = 0; + objStoreMeta meta; + + if (nats_IsStringEmpty(data)) + data = NULL; + else + len = (int) strlen(data); + + objStoreMeta_Init(&meta); + meta.Name = name; + s = objStore_Put(&put, obs, &meta); + IFOK(s, objStorePut_Add(put, (const void*) data, len)); + IFOK(s, objStorePut_Complete(new_info, put, 0)); + objStorePut_Destroy(put); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_PutBytes(objStoreInfo **new_info, objStore *obs, const char *name, const void *data, int dataLen) +{ + natsStatus s = NATS_OK; + objStorePut *put = NULL; + objStoreMeta meta; + + objStoreMeta_Init(&meta); + meta.Name = name; + s = objStore_Put(&put, obs, &meta); + IFOK(s, objStorePut_Add(put, data, dataLen)); + IFOK(s, objStorePut_Complete(new_info, put, 0)); + objStorePut_Destroy(put); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_PutFile(objStoreInfo **new_info, objStore *obs, const char *fileName) +{ + natsStatus s = NATS_OK; + FILE *f = NULL; + objStorePut *put = NULL; + void *chunk = NULL; + objStoreMeta meta; + + if ((obs == NULL) || nats_IsStringEmpty(fileName)) + return nats_setDefaultError(NATS_INVALID_ARG); + + f = fopen(fileName, "r"); + if (f == NULL) + return nats_setError(NATS_ERR, "error opening file '%s': %d (%s)", + fileName, errno, strerror(errno)); + + objStoreMeta_Init(&meta); + meta.Name = fileName; + s = objStore_Put(&put, obs, &meta); + if (s == NATS_OK) + { + chunk = NATS_MALLOC(obsDefaultChunkSize); + if (chunk == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + while (s == NATS_OK) + { + size_t n = fread(chunk, 1, (size_t) obsDefaultChunkSize, f); + if (n == 0) + break; + + s = objStorePut_Add(put, (const void*) chunk, (int) n); + } + IFOK(s, objStorePut_Complete(new_info, put, 0)); + + fclose(f); + objStorePut_Destroy(put); + NATS_FREE(chunk); + + return NATS_UPDATE_ERR_STACK(s); +} + +////////////////////////////////////////////////////////////////////////////// +// objStore GET APIs +////////////////////////////////////////////////////////////////////////////// + +void +objStoreGet_Destroy(objStoreGet *get) +{ + if (get == NULL) + return; + + objStoreInfo_Destroy(get->info); + nats_hashDestroy(get->digest); + natsSubscription_Destroy(get->sub); + _releaseObs(get->obs); + NATS_FREE(get); +} + +static natsStatus +_get(objStoreGet **new_get, objStore *obs, int64_t start, int64_t timeout, const char *name, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + objStoreInfo *info = NULL; + objStoreGet *get = NULL; + char *chunkSubj = NULL; + + // Grab meta info. + s = _getInfo(&info, obs, start, timeout, name, opts); + if (s != NATS_OK) + { + if (s == NATS_NOT_FOUND) + return s; + return NATS_UPDATE_ERR_STACK(s); + } + if (nats_IsStringEmpty(info->NUID)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrBadObjectMeta); + else if (nats_asprintf(&chunkSubj, objChunksPreTmpl, obs->name, info->NUID) < 0) + s = nats_setDefaultError(NATS_NO_MEMORY); + + // Check for object links. If single objects we do a pass through. + if ((s == NATS_OK) && _isLink(info)) + { + // We know that info->Meta.Opts.Link != NULL. + if (nats_IsStringEmpty(info->Meta.Opts.Link->Name)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrCantGetBucket); + if (s == NATS_OK) + { + const char *lbuck = info->Meta.Opts.Link->Bucket; + + if (nats_IsStringEmpty(lbuck)) + s = nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrBadObjectMeta); + else + { + // is the link in the same bucket? + if (strcmp(lbuck, obs->name) == 0) + { + s = _get(new_get, obs, start, timeout, info->Meta.Opts.Link->Name, opts); + } + else + { + objStore *lobs = NULL; + + // different bucket + s = js_ObjectStore(&lobs, obs->js, lbuck); + if (s == NATS_OK) + { + s = _get(new_get, lobs, start, timeout, info->Meta.Opts.Link->Name, opts); + + // Destroy the `lobs` object now. + objStore_Destroy(lobs); + } + } + } + } + // Destroy the meta's info object we got at the top of this function. + objStoreInfo_Destroy(info); + + // Free this now. + NATS_FREE(chunkSubj); + + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); + } + if (s == NATS_OK) + { + get = (objStoreGet*) NATS_CALLOC(1, sizeof(objStoreGet)); + if (get == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + else + _retainObs(obs); + } + if (s == NATS_OK) + { + get->obs = obs; + get->info = info; + get->remaining = info->Size; + + if (info->Size == 0) + { + // Free this now. + NATS_FREE(chunkSubj); + + *new_get = get; + return NATS_OK; + } + // Since now `get` owns `info`, let's clear `info` reference so that we can + // do proper cleanup at the end in case of error. + info = NULL; + } + IFOK(s, nats_hashNew(&(get->digest))); + if (s == NATS_OK) + { + jsOptions jo; + jsSubOptions so; + int64_t elapsed = 0; + + s = _checkElapsed(&elapsed, start, timeout); + if (s == NATS_OK) + { + jsOptions_Init(&jo); + jo.Wait = timeout-elapsed; + + jsSubOptions_Init(&so); + so.Ordered = true; + so.Stream = obs->streamName; + // Use pushJS here. + s = js_SubscribeSync(&(get->sub), obs->pushJS, chunkSubj, &jo, &so, NULL); + IFOK(s, natsSubscription_SetPendingLimits(get->sub, -1, -1)); + } + } + // Free this, regardless of status. + NATS_FREE(chunkSubj); + + // Success, return the `get` object. + if (s == NATS_OK) + *new_get = get; + else + { + // In case it was not associated with the `get` object. + objStoreInfo_Destroy(info); + objStoreGet_Destroy(get); + } + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_Get(objStoreGet **new_get, objStore *obs, const char *name, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + int64_t start = nats_Now(); + int64_t timeout = 0; + + if ((new_get == NULL) || (obs == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + // Get the max duration based on the js context's Wait time (expressed in ms). + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + s = _get(new_get, obs, start, timeout, name, opts); + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStoreGet_Info(const objStoreInfo **info, objStoreGet *get) +{ + if ((info == NULL) || (get == NULL)) + return nats_setDefaultError(NATS_INVALID_ARG); + + *info = get->info; + return NATS_OK; +} + +static natsStatus +_readInto(bool *done, void **new_data, void *pdata, int *dataLen, objStoreGet *get, bool alloc, int64_t timeout) +{ + natsStatus s = NATS_OK; + natsMsg *msg = NULL; + void *data = NULL; + int len = 0; + + // Check if we are done, if so, return error. + if (get->done) + return nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrReadComplete); + + // It could be that there is no data to read. + if (get->remaining == 0) + { + *done = true; + get->done = true; + return NATS_OK; + } + + s = natsSubscription_NextMsg(&msg, get->sub, timeout); + if (s == NATS_OK) + { + const char *mdata = natsMsg_GetData(msg); + + len = natsMsg_GetDataLength(msg); + if ((uint64_t) len > get->remaining) + s = nats_setError(NATS_ILLEGAL_STATE, "expected remaining %" PRIu64 " bytes, got %d", get->remaining, len); + else + { + if (alloc) + { + data = NATS_MALLOC(len); + if (data == NULL) + s = nats_setDefaultError(NATS_NO_MEMORY); + } + else + { + data = pdata; + } + if (s == NATS_OK) + { + s = nats_hashWrite(get->digest, (const void*) mdata, len); + if (s == NATS_OK) + { + memcpy(data, (const void*) mdata, len); + get->remaining -= (uint64_t) len; + } + } + } + } + if (s == NATS_OK) + { + // Mark as done if no more bytes to read. + get->done = (get->remaining == 0 ? true : false); + if (get->done) + { + char *digest = NULL; + + s = _getDigestValue(&digest, get->digest); + if ((s == NATS_OK) && (strcmp((const char*) digest, get->info->Digest) != 0)) + s = nats_setError(NATS_ERR, "%s", obsErrDigestMismatch); + + NATS_FREE(digest); + } + } + natsMsg_Destroy(msg); + if (s == NATS_OK) + { + if (alloc) + *new_data = data; + *dataLen = len; + *done = get->done; + } + else if (alloc) + { + NATS_FREE(data); + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStoreGet_Read(bool *done, void **new_data, int *dataLen, objStoreGet *get, int64_t timeout) +{ + natsStatus s = NATS_OK; + + if ((done == NULL) || (new_data == NULL) || (dataLen == NULL) || (get == NULL) || (timeout <= 0)) + return nats_setDefaultError(NATS_INVALID_ARG); + + // Initialize the output. + *new_data = NULL; + *dataLen = 0; + *done = false; + + s = _readInto(done, new_data, NULL, dataLen, get, true, timeout); + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_readAll(void **new_data, int *dataLen, objStoreGet *get, bool forString, int64_t timeout) +{ + natsStatus s = NATS_OK; + int64_t start = nats_Now(); + int64_t elapsed = 0; + void *data = NULL; + void *pdata = NULL; + int len = 0; + + if ((new_data == NULL) || (dataLen == NULL) || (get == NULL) || (timeout <= 0)) + return nats_setDefaultError(NATS_INVALID_ARG); + + // Initialize the output. + *new_data = NULL; + *dataLen = 0; + + if (get->done) + return nats_setError(NATS_ILLEGAL_STATE, "%s", obsErrReadComplete); + + len = (int) get->remaining; + if (len == 0) + { + get->done = true; + return NATS_OK; + } + + // Alloc for the remaining of bytes that need to be read. + data = NATS_MALLOC(len + (forString ? 1 : 0)); + if (data == NULL) + return nats_setDefaultError(NATS_NO_MEMORY); + + pdata = data; + while ((s = _checkElapsed(&elapsed, start, timeout)) == NATS_OK) + { + bool done = false; + int cl = 0; + + s = _readInto(&done, NULL, pdata, &cl, get, false, timeout-elapsed); + if (s == NATS_OK) + { + pdata = (void*) (((char*) pdata) + cl); + if (done) + break; + } + } + if (s == NATS_OK) + { + if (forString) + ((char*) data)[len] = '\0'; + + *new_data = data; + *dataLen = len; + } + else + { + NATS_FREE(data); + } + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStoreGet_ReadAll(void **new_data, int *dataLen, objStoreGet *get, int64_t timeout) +{ + natsStatus s; + + s = _readAll(new_data, dataLen, get, false, timeout); + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +static natsStatus +_getBytes(void **new_data, int *dataLen, objStore *obs, bool forString, const char *name, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + objStoreGet *get = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + + if (obs == NULL) + return nats_setDefaultError(NATS_INVALID_ARG); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + s = objStore_Get(&get, obs, name, opts); + IFOK(s, _checkElapsed(&elapsed, start, timeout)); + IFOK(s, _readAll(new_data, dataLen, get, forString, timeout-elapsed)); + + objStoreGet_Destroy(get); + + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_GetString(char **new_str, objStore *obs, const char *name, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + int len = 0; + + s = _getBytes((void**) new_str, &len, obs, true, name, opts); + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_GetBytes(void **new_data, int *dataLen, objStore *obs, const char *name, objStoreOptions *opts) +{ + natsStatus s; + + s = _getBytes(new_data, dataLen, obs, false, name, opts); + if (s == NATS_NOT_FOUND) + return s; + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +objStore_GetFile(objStore *obs, const char *name, const char *fileName, objStoreOptions *opts) +{ + natsStatus s = NATS_OK; + FILE *f = NULL; + objStoreGet *get = NULL; + int64_t start = nats_Now(); + int64_t elapsed = 0; + int64_t timeout = 0; + + if ((obs == NULL) || nats_IsStringEmpty(fileName)) + return nats_setDefaultError(NATS_INVALID_ARG); + + f = fopen(fileName, "w"); + if (f == NULL) + return nats_setError(NATS_ERR, "error opening file '%s': %d (%s)", + fileName, errno, strerror(errno)); + + js_lock(obs->js); + timeout = obs->js->opts.Wait; + js_unlock(obs->js); + + s = objStore_Get(&get, obs, name, opts); + if (s == NATS_OK) + { + while ((s = _checkElapsed(&elapsed, start, timeout)) == NATS_OK) + { + void *data = NULL; + int len = 0; + bool done = false; + + s = objStoreGet_Read(&done, &data, &len, get, timeout-elapsed); + if (s == NATS_OK) + { + fwrite((const void*) data, 1, (size_t) len, f); + if (ferror(f)) + { + s = nats_setError(NATS_ERR, "error writing into file '%s': %d (%s)", + fileName, errno, strerror(errno)); + } + } + // Free the data now. + NATS_FREE(data); + if ((s == NATS_OK) && done) + break; + } + } + // Close the file. + if (fclose(f)) + { + s = nats_setError(NATS_ERR, "error closing file '%s': %d (%s)", + fileName, errno, strerror(errno)); + } + // Destroy the `get` object now. + objStoreGet_Destroy(get); + + if (s != NATS_OK) + { + // On error, we will delete the file. + remove(fileName); + // On "not found" error, do not update the error stack. + if (s == NATS_NOT_FOUND) + return s; + } + return NATS_UPDATE_ERR_STACK(s); +} diff --git a/src/object.h b/src/object.h new file mode 100644 index 000000000..054652206 --- /dev/null +++ b/src/object.h @@ -0,0 +1,32 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define obsErrInvalidStoreName "invalid object-store name" +#define obsErrBucketExists "bucket name already in use" +#define obsErrBucketNotFound "bucket not found" +#define obsErrBadObjectMeta "object-store meta information invalid" +#define obsErrNameIsRequired "name is required" +#define obsErrLinkNotAllowed "link cannot be set when putting the object in bucket" +#define obsErrCantGetBucket "invalid get operation, object is a link to a bucket" +#define obsErrDigestMismatch "received a corrupt object, digests do not match" +#define obsErrReadComplete "no more data to read from the object" +#define obsErrUpdateMetaDelete "cannot update meta for a deleted or not found object" +#define obsErrObjectAlreadyExists "an object already exists with that name" +#define obsErrNoLinkToDeleted "not allowed to link to a deleted object" +#define obsErrNoLinkToLink "not allowed to link to another link" + +#define obsDefaultChunkSize (uint32_t)(128 * 1024) + +#define obsInitialListCapValue 16 + +extern int obsInitialListCap; diff --git a/src/util.c b/src/util.c index c414410f0..9f3b631e7 100644 --- a/src/util.c +++ b/src/util.c @@ -1242,6 +1242,12 @@ nats_JSONGetUInt16(nats_JSON *json, const char *fieldName, uint16_t *value) JSON_GET_AS(TYPE_UINT, uint16_t); } +natsStatus +nats_JSONGetUInt32(nats_JSON *json, const char *fieldName, uint32_t *value) +{ + JSON_GET_AS(TYPE_UINT, uint32_t); +} + natsStatus nats_JSONGetBool(nats_JSON *json, const char *fieldName, bool *value) { @@ -1865,6 +1871,13 @@ nats_Base64RawURL_EncodeString(const unsigned char *src, int srcLen, char **pDes return NATS_UPDATE_ERR_STACK(s); } +natsStatus +nats_Base64URL_EncodeString(const unsigned char *src, int srcLen, char **pDest) +{ + natsStatus s = _base64Encode(base64EncodeURL, true, src, srcLen, pDest); + return NATS_UPDATE_ERR_STACK(s); +} + natsStatus nats_Base64_Encode(const unsigned char *src, int srcLen, char **pDest) { @@ -2472,27 +2485,27 @@ nats_validateLimitedTerm(const char *name, const char *term) } natsStatus -nats_marshalMetadata(natsBuffer *buf, bool comma, const char *fieldName, natsMetadata md) +nats_marshalMetadata(natsBuffer *buf, bool comma, const char *fieldName, natsMetadata *md) { natsStatus s = NATS_OK; int i; const char *start = (comma ? ",\"" : "\""); - if (md.Count <= 0) + if ((md == NULL) || (md->Count <= 0)) return NATS_OK; IFOK(s, natsBuf_Append(buf, start, -1)); IFOK(s, natsBuf_Append(buf, fieldName, -1)); IFOK(s, natsBuf_Append(buf, "\":{", 3)); - for (i = 0; (s == NATS_OK) && (i < md.Count); i++) + for (i = 0; (s == NATS_OK) && (i < md->Count); i++) { IFOK(s, natsBuf_AppendByte(buf, '"')); - IFOK(s, natsBuf_Append(buf, md.List[i * 2], -1)); + IFOK(s, natsBuf_Append(buf, md->List[i * 2], -1)); IFOK(s, natsBuf_Append(buf, "\":\"", 3)); - IFOK(s, natsBuf_Append(buf, md.List[i * 2 + 1], -1)); + IFOK(s, natsBuf_Append(buf, md->List[i * 2 + 1], -1)); IFOK(s, natsBuf_AppendByte(buf, '"')); - if (i != md.Count - 1) + if (i != md->Count - 1) IFOK(s, natsBuf_AppendByte(buf, ',')); } IFOK(s, natsBuf_AppendByte(buf, '}')); @@ -2545,26 +2558,26 @@ nats_unmarshalMetadata(nats_JSON *json, const char *fieldName, natsMetadata *md) } natsStatus -nats_cloneMetadata(natsMetadata *clone, natsMetadata md) +nats_cloneMetadata(natsMetadata *clone, natsMetadata *md) { - natsStatus s = NATS_OK; - int i = 0; - int n; - char **list = NULL; + natsStatus s = NATS_OK; + char **list = NULL; + int i = 0; + int n; clone->Count = 0; clone->List = NULL; - if (md.Count == 0) + if ((md == NULL) || (md->Count == 0)) return NATS_OK; - n = md.Count * 2; + n = md->Count * 2; list = NATS_CALLOC(n, sizeof(char *)); if (list == NULL) s = nats_setDefaultError(NATS_NO_MEMORY); for (i = 0; (s == NATS_OK) && (i < n); i++) { - list[i] = NATS_STRDUP(md.List[i]); + list[i] = NATS_STRDUP(md->List[i]); if (list[i] == NULL) s = nats_setDefaultError(NATS_NO_MEMORY); } @@ -2572,7 +2585,7 @@ nats_cloneMetadata(natsMetadata *clone, natsMetadata md) if (s == NATS_OK) { clone->List = (const char **)list; - clone->Count = md.Count; + clone->Count = md->Count; } else { @@ -2624,13 +2637,13 @@ natsStatus nats_formatStringArray(char **out, const char **strings, int count) natsStatus nats_marshalStringArray(natsBuffer *buf, bool comma, const char *fieldName, const char **values, int len) { - natsStatus s = NATS_OK; - int i; - const char *sep = (comma ? "," : ""); + natsStatus s = NATS_OK; + int i; if (!nats_IsStringEmpty(fieldName)) { - s = natsBuf_Append(buf, sep, -1); + if (comma) + s = natsBuf_AppendByte(buf, ','); IFOK(s, natsBuf_AppendByte(buf, '"')); IFOK(s, natsBuf_Append(buf, fieldName, -1)); IFOK(s, natsBuf_AppendByte(buf, '"')); @@ -2660,3 +2673,152 @@ nats_marshalStringArray(natsBuffer *buf, bool comma, const char *fieldName, cons return NATS_UPDATE_ERR_STACK(s); } +natsStatus +nats_marshalTimeUTC(natsBuffer *buf, bool comma, const char *fieldName, int64_t timeUTC) +{ + natsStatus s = NATS_OK; + char dbuf[36] = {'\0'}; + + s = nats_EncodeTimeUTC(dbuf, sizeof(dbuf), timeUTC); + if (s != NATS_OK) + return nats_setError(NATS_ERR, "unable to encode data for field '%s' value %" PRId64, fieldName, timeUTC); + + if (comma) + s = natsBuf_AppendByte(buf, ','); + + IFOK(s, natsBuf_AppendByte(buf, '"')); + IFOK(s, natsBuf_Append(buf, fieldName, -1)); + IFOK(s, natsBuf_Append(buf, "\":\"", -1)); + IFOK(s, natsBuf_Append(buf, dbuf, -1)); + IFOK(s, natsBuf_AppendByte(buf, '"')); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +nats_marshalString(natsBuffer *buf, bool omitEmpty, bool comma, const char *fieldName, const char *str) +{ + natsStatus s = NATS_OK; + + if (omitEmpty && nats_IsStringEmpty(str)) + return NATS_OK; + + if (comma) + s = natsBuf_AppendByte(buf, ','); + + IFOK(s, natsBuf_AppendByte(buf, '"')); + IFOK(s, natsBuf_Append(buf, fieldName, -1)); + IFOK(s, natsBuf_Append(buf, "\":\"", -1)); + IFOK(s, natsBuf_Append(buf, str, -1)); + IFOK(s, natsBuf_AppendByte(buf, '"')); + + return NATS_UPDATE_ERR_STACK(s); +} + +bool +nats_validBucketName(const char *bucket) +{ + int i; + char c; + + if (nats_IsStringEmpty(bucket)) + return false; + + for (i=0; i<(int)strlen(bucket); i++) + { + c = bucket[i]; + if ((isalnum((unsigned char) c) == 0) && (c != '_') && (c != '-')) + return false; + } + return true; +} + +static natsStatus +_addHeader(void *userInfo, const char *fieldName, nats_JSONField *f) +{ + natsStatus s = NATS_OK; + natsHeader *h = (natsHeader*) userInfo; + nats_JSONArray *arr = f->value.varr; + int i; + + for (i=0; (s == NATS_OK) && (isize); i++) + s = natsHeader_Add(h, fieldName, (const char*) arr->values[i]); + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +nats_unmarshalHeader(nats_JSON *json, const char *fieldName, natsHeader **new_header) +{ + natsStatus s = NATS_OK; + nats_JSON *hJSON = NULL; + natsHeader *h = NULL; + + *new_header = NULL; + + s = nats_JSONGetObject(json, fieldName, &hJSON); + if (s != NATS_OK) + return NATS_UPDATE_ERR_STACK(s); + + if ((hJSON == NULL) || (natsStrHash_Count(hJSON->fields) == 0)) + return NATS_OK; + + s = natsHeader_New(&h); + IFOK(s, nats_JSONRange(hJSON, TYPE_ARRAY, 0, _addHeader, (void*) h)); + if (s == NATS_OK) + *new_header = h; + else + natsHeader_Destroy(h); + + return NATS_UPDATE_ERR_STACK(s); +} + +natsStatus +nats_marshalHeader(natsBuffer *buf, bool omitEmpty, bool comma, const char *fieldName, natsHeader *header) +{ + natsStatus s = NATS_OK; + + // The function returns 0 if header is NULL. + if (omitEmpty && (natsHeader_KeysCount(header) == 0)) + return NATS_OK; + + if (comma) + s = natsBuf_AppendByte(buf, ','); + IFOK(s, natsBuf_AppendByte(buf, '\"')); + IFOK(s, natsBuf_Append(buf, fieldName, -1)); + IFOK(s, natsBuf_Append(buf, "\":{", -1)); + if (s == NATS_OK) + { + natsStrHashIter iter; + char *key = NULL; + void *p = NULL; + natsHeaderValue *v = NULL; + bool first= true; + + comma = false; + natsStrHashIter_Init(&iter, (natsStrHash*) header); + while ((s == NATS_OK) && natsStrHashIter_Next(&iter, &key, &p)) + { + if (!first) + s = natsBuf_AppendByte(buf, ','); + IFOK(s, natsBuf_AppendByte(buf, '\"')); + IFOK(s, natsBuf_Append(buf, key, -1)); + IFOK(s, natsBuf_Append(buf, "\":[", -1)); + + comma = false; + for (v = (natsHeaderValue*) p; (s == NATS_OK) && (v != NULL); v = v->next) + { + if (comma) + s = natsBuf_AppendByte(buf, ','); + IFOK(s, natsBuf_AppendByte(buf, '\"')); + IFOK(s, natsBuf_Append(buf, v->value, -1)); + IFOK(s, natsBuf_AppendByte(buf, '\"')); + comma = true; + } + IFOK(s, natsBuf_AppendByte(buf, ']')); + first = false; + } + } + IFOK(s, natsBuf_AppendByte(buf, '}')); + + return NATS_UPDATE_ERR_STACK(s); +} diff --git a/src/util.h b/src/util.h index d7bd0d467..dd5047d22 100644 --- a/src/util.h +++ b/src/util.h @@ -117,6 +117,9 @@ nats_JSONGetInt32(nats_JSON *json, const char *fieldName, int32_t *value); natsStatus nats_JSONGetUInt16(nats_JSON *json, const char *fieldName, uint16_t *value); +natsStatus +nats_JSONGetUInt32(nats_JSON *json, const char *fieldName, uint32_t *value); + natsStatus nats_JSONGetBool(nats_JSON *json, const char *fieldName, bool *value); @@ -204,6 +207,9 @@ nats_Base32_DecodeString(const char *src, char *dst, int dstMax, int *dstLen); natsStatus nats_Base64RawURL_EncodeString(const unsigned char *src, int srcLen, char **pDest); +natsStatus +nats_Base64URL_EncodeString(const unsigned char *src, int srcLen, char **pDest); + natsStatus nats_Base64_Encode(const unsigned char *src, int srcLen, char **pDest); @@ -252,13 +258,13 @@ natsStatus nats_marshalDuration(natsBuffer *out_buf, bool comma, const char *field_name, int64_t d); natsStatus -nats_marshalMetadata(natsBuffer *buf, bool comma, const char *fieldName, natsMetadata md); +nats_marshalMetadata(natsBuffer *buf, bool comma, const char *fieldName, natsMetadata *md); natsStatus nats_unmarshalMetadata(nats_JSON *json, const char *fieldName, natsMetadata *md); natsStatus -nats_cloneMetadata(natsMetadata *clone, natsMetadata md); +nats_cloneMetadata(natsMetadata *clone, natsMetadata *md); void nats_freeMetadata(natsMetadata *md); @@ -275,7 +281,22 @@ nats_formatStringArray(char **out, const char **strings, int count); natsStatus nats_marshalStringArray(natsBuffer *buf, bool comma, const char *fieldName, const char **values, int len); +natsStatus +nats_marshalTimeUTC(natsBuffer *buf, bool comma, const char *fieldName, int64_t timeUTC); + +natsStatus +nats_marshalString(natsBuffer *buf, bool omitEmpty, bool comma, const char *fieldName, const char *str); + natsStatus nats_validateLimitedTerm(const char *name, const char *term); +bool +nats_validBucketName(const char *bucket); + +natsStatus +nats_unmarshalHeader(nats_JSON *json, const char *fieldName, natsHeader **header); + +natsStatus +nats_marshalHeader(natsBuffer *buf, bool omitEmpty, bool comma, const char *fieldName, natsHeader *header); + #endif /* UTIL_H_ */ diff --git a/test/list_test.txt b/test/list_test.txt index dca4a0e00..0215343da 100644 --- a/test/list_test.txt +++ b/test/list_test.txt @@ -165,6 +165,7 @@ _test(natsFormatStringArray) _test(natsGetJWTOrSeed) _test(natsHash) _test(natsHashing) +_test(natsHeader) _test(natsHostIsIP) _test(natsInbox) _test(natsJSON) @@ -204,6 +205,11 @@ _test(NoEchoOldServer) _test(NoFlusherIfSendAsap) _test(NoPartialOnReconnect) _test(NoResponders) +_test(ObjectStore_GlobalMgt) +_test(ObjectStore_PutAndGet) +_test(ObjectStore_StoreMgt) +_test(ObjectStore_StoreMgtLinks) +_test(ObjectStore_StoreMgtWatchAndList) _test(OldRequest) _test(OpenCloseAndWait) _test(ParseINFO) diff --git a/test/test.c b/test/test.c index c9b12537a..e71a4e4f7 100644 --- a/test/test.c +++ b/test/test.c @@ -40,6 +40,7 @@ #include "kv.h" #include "microp.h" #include "glib/glibp.h" +#include "object.h" #if defined(NATS_HAS_STREAMING) @@ -4619,11 +4620,16 @@ void test_natsBase64Encode(void) "dfslfdlkjsfdllkjfds dfsjlklkfsda dfsalkjklfdsalkj adfskjllkjfdaslkjfdslk", "This is another with numbers like 12345678.90 and special characters !@#$%^&*()-=+/", }; - const char *expectedResults[] = { + const char *expectedRawResults[] = { "dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw", "ZGZzbGZkbGtqc2ZkbGxramZkcyBkZnNqbGtsa2ZzZGEgZGZzYWxramtsZmRzYWxraiBhZGZza2psbGtqZmRhc2xramZkc2xr", "VGhpcyBpcyBhbm90aGVyIHdpdGggbnVtYmVycyBsaWtlIDEyMzQ1Njc4LjkwIGFuZCBzcGVjaWFsIGNoYXJhY3RlcnMgIUAjJCVeJiooKS09Ky8", }; + const char *expectedResults[] = { + "dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw==", + "ZGZzbGZkbGtqc2ZkbGxramZkcyBkZnNqbGtsa2ZzZGEgZGZzYWxramtsZmRzYWxraiBhZGZza2psbGtqZmRhc2xramZkc2xr", + "VGhpcyBpcyBhbm90aGVyIHdpdGggbnVtYmVycyBsaWtlIDEyMzQ1Njc4LjkwIGFuZCBzcGVjaWFsIGNoYXJhY3RlcnMgIUAjJCVeJiooKS09Ky8=", + }; const char *expectedResultsStd[] = { "dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw==", "ZGZzbGZkbGtqc2ZkbGxramZkcyBkZnNqbGtsa2ZzZGEgZGZzYWxramtsZmRzYWxraiBhZGZza2psbGtqZmRhc2xramZkc2xr", @@ -4636,19 +4642,19 @@ void test_natsBase64Encode(void) int dl = 0; unsigned char *dec = NULL; - test("EncodeURL nil: "); + test("EncodeRawURL nil: "); s = nats_Base64RawURL_EncodeString(NULL, 0, &enc); testCond((s == NATS_OK) && (enc == NULL)); - test("EncodeURL empty: "); + test("EncodeRawURL empty: "); s = nats_Base64RawURL_EncodeString((const unsigned char*) "", 0, &enc); testCond((s == NATS_OK) && (enc == NULL)); - test("EncodeURL strings: "); + test("EncodeRawURL strings: "); for (i=0; i<(int)(sizeof(testStrings)/sizeof(char*)); i++) { s = nats_Base64RawURL_EncodeString((const unsigned char*) testStrings[i], (int)strlen(testStrings[i]), &enc); - if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, expectedResults[i]) != 0))) + if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, expectedRawResults[i]) != 0))) s = NATS_ERR; free(enc); @@ -4658,7 +4664,7 @@ void test_natsBase64Encode(void) } testCond(s == NATS_OK); - test("EncodeURL bytes: "); + test("EncodeRawURL bytes: "); { s = nats_Base64RawURL_EncodeString((const unsigned char*) &someBytes, sbl, &enc); if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, "AAIAAwQFAAYHCAAJAA") != 0))) @@ -4669,6 +4675,39 @@ void test_natsBase64Encode(void) } testCond(s == NATS_OK); + test("EncodeURL nil: "); + s = nats_Base64URL_EncodeString(NULL, 0, &enc); + testCond((s == NATS_OK) && (enc == NULL)); + + test("EncodeURL empty: "); + s = nats_Base64URL_EncodeString((const unsigned char*) "", 0, &enc); + testCond((s == NATS_OK) && (enc == NULL)); + + test("EncodeURL strings: "); + for (i=0; i<(int)(sizeof(testStrings)/sizeof(char*)); i++) + { + s = nats_Base64URL_EncodeString((const unsigned char*) testStrings[i], (int)strlen(testStrings[i]), &enc); + if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, expectedResults[i]) != 0))) + s = NATS_ERR; + + free(enc); + enc = NULL; + if (s != NATS_OK) + break; + } + testCond(s == NATS_OK); + + test("EncodeURL bytes: "); + { + s = nats_Base64URL_EncodeString((const unsigned char*) &someBytes, sbl, &enc); + if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, "AAIAAwQFAAYHCAAJAA==") != 0))) + s = NATS_ERR; + + free(enc); + enc = NULL; + } + testCond(s == NATS_OK); + test("Encode nil: "); s = nats_Base64_Encode(NULL, 0, &enc); testCond((s == NATS_OK) && (enc == NULL)); @@ -24051,8 +24090,8 @@ void test_JetStreamMgtStreams(void) test("Purge stream (not found): "); s = js_PurgeStream(js, "NOT_FOUND", NULL, &jerr); testCond((s == NATS_NOT_FOUND) - && (jerr == JSStreamNotFoundErr)); - nats_clearLastError(); + && (jerr == JSStreamNotFoundErr) + && (nats_GetLastError(NULL) == NULL)); test("Get stream info (verify purged): "); s = js_GetStreamInfo(&si, js, "TEST2", NULL, &jerr); @@ -24256,8 +24295,8 @@ void test_JetStreamMgtStreams(void) test("Delete stream (not found): "); s = js_DeleteStream(js, "NOT_FOUND", NULL, &jerr); testCond((s == NATS_NOT_FOUND) - && (jerr == JSStreamNotFoundErr)); - nats_clearLastError(); + && (jerr == JSStreamNotFoundErr) + && (nats_GetLastError(NULL) == NULL)); test("Delete stream: "); s = js_DeleteStream(js, "TEST2", NULL, &jerr); @@ -24267,8 +24306,8 @@ void test_JetStreamMgtStreams(void) s = js_GetStreamInfo(&si, js, "TEST2", NULL, &jerr); testCond((s == NATS_NOT_FOUND) && (jerr == JSStreamNotFoundErr) - && (si == NULL)); - nats_clearLastError(); + && (si == NULL) + && (nats_GetLastError(NULL) == NULL)); natsSubscription_Destroy(sub); sub = NULL; @@ -25097,8 +25136,8 @@ void test_JetStreamMgtConsumers(void) test("Delete consumer (not found): "); s = js_DeleteConsumer(js, "MY_STREAM", "dur2", NULL, &jerr); testCond((s == NATS_NOT_FOUND) - && (jerr == JSConsumerNotFoundErr)); - nats_clearLastError(); + && (jerr == JSConsumerNotFoundErr) + && (nats_GetLastError(NULL) == NULL)); test("Create consumer with description: "); jsConsumerConfig_Init(&cfg); @@ -34720,6 +34759,2229 @@ void test_KeyValueMirrorCrossDomains(void) remove(lconfFile); } +void test_natsHeader(void) +{ + natsStatus s = NATS_OK; + natsHeader *h = NULL; + const char **keys = NULL; + const char *val = NULL; + const char **values = NULL; + int count = 0; + const char *longKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + test("New (bad args): "); + s = natsHeader_New(NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("New: "); + s = natsHeader_New(&h); + testCond((s == NATS_OK) && (h != NULL)); + + test("Key cannot be NULL: "); + s = natsHeader_Set(h, NULL, "value"); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Key cannot be empty: "); + s = natsHeader_Set(h, "", "value"); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Set msg cannot be NULL: "); + s = natsHeader_Set(NULL, "key", "value"); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Set value: "); + s = natsHeader_Set(h, "my-key", "value1"); + testCond(s == NATS_OK); + + test("Get header cannot be NULL: "); + s = natsHeader_Get(NULL, "my-key", &val); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Get must provide mem location: "); + s = natsHeader_Get(h, "my-key", NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Get: "); + s = natsHeader_Get(h, "my-key", &val); + testCond((s == NATS_OK) && + (val != NULL) && + (strcmp(val, "value1") == 0)); + val = NULL; + + test("Get value with different case: "); + s = natsHeader_Get(h, "my-Key", &val); + testCond((s == NATS_NOT_FOUND) && (val == NULL)); + + test("Key not found: "); + s = natsHeader_Get(h, "unknown-key", &val); + testCond((s == NATS_NOT_FOUND) && (val == NULL)); + + test("Set value replace old: "); + s = natsHeader_Set(h, "my-key", "value2"); + testCond(s == NATS_OK); + + test("Get value: "); + s = natsHeader_Get(h, "my-key", &val); + testCond((s == NATS_OK) && + (val != NULL) && + (strcmp(val, "value2") == 0)); + val = NULL; + + test("Set value cannot be NULL: "); + s = natsHeader_Set(h, "setnullvalue", NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Get value: "); + s = natsHeader_Get(h, "setnullvalue", &val); + testCond((s == NATS_NOT_FOUND) && (val == NULL)); + + test("Set empty value: "); + s = natsHeader_Set(h, "my-key", ""); + testCond(s == NATS_OK); + + test("Get value: "); + s = natsHeader_Get(h, "my-key", &val); + testCond((s == NATS_OK) && (val != NULL) && (val[0] == '\0')); + val = NULL; + + test("Add header cannot be NULL: "); + s = natsHeader_Add(NULL, "key", "value"); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Add first: "); + s = natsHeader_Add(h, "two-fields", "val1"); + testCond(s == NATS_OK); + + test("Add second: "); + s = natsHeader_Add(h, "two-fields", "val2"); + testCond(s == NATS_OK); + + test("Get should return first: "); + s = natsHeader_Get(h, "two-fields", &val); + testCond((s == NATS_OK) && + (val != NULL) && + (strcmp(val, "val1") == 0)); + val = NULL; + + test("Values: "); + s = natsHeader_Values(h, "two-fields", &values, &count); + testCond((s == NATS_OK) && (values != NULL) && (count == 2) && + (strcmp(values[0], "val1") == 0) && + (strcmp(values[1], "val2") == 0)); + + if (values != NULL) + free((void*) values); + values = NULL; + count = 0; + + test("Add after a Set: "); + s = natsHeader_Set(h, "my-other-key", "val3"); + IFOK(s, natsHeader_Add(h, "my-other-key", "val4")); + IFOK(s, natsHeader_Values(h, "my-other-key", &values, &count)); + testCond((s == NATS_OK) && (values != NULL) && (count == 2) && + (strcmp(values[0], "val3") == 0) && + (strcmp(values[1], "val4") == 0)); + + if (values != NULL) + free((void*) values); + values = NULL; + count = 0; + + test("Keys header cannot be NULL: "); + s = natsHeader_Keys(NULL, &keys, &count); + testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0)); + if (s == NATS_INVALID_ARG) + { + s = NATS_OK; + nats_clearLastError(); + } + + test("Keys keys cannot be NULL: "); + s = natsHeader_Keys(h, NULL, &count); + testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0)); + nats_clearLastError(); + + test("Keys count cannot be NULL: "); + s = natsHeader_Keys(h, &keys, NULL); + testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0)); + nats_clearLastError(); + + test("KeysCount (NULL): "); + testCond(natsHeader_KeysCount(NULL) == 0); + + test("KeysCount: "); + testCond(natsHeader_KeysCount(h) == 3); + + test("Keys: "); + s = natsHeader_Keys(h, &keys, &count); + if ((s == NATS_OK) && ((keys == NULL) || (count != 3))) + { + s = NATS_ERR; + } + else + { + int i; + bool ok1 = false; + bool ok2 = false; + bool ok3 = false; + + for (i=0; iConfig->Description, "Testing ObjectStore") == 0) + && (si->Config->Storage == js_MemoryStorage) + && (si->Config->MaxAge == NATS_SECONDS_TO_NANOS(10)) + && (si->Config->MaxBytes == 1024*1024*1024) + && (si->Config->Metadata.Count >= 2)); // There may be other meta data fields. + + test("Check metadata: "); + for (i=0; i<2*si->Config->Metadata.Count; i+=2) + { + if ((strcmp(si->Config->Metadata.List[i], "field1") == 0) + && (strcmp(si->Config->Metadata.List[i+1], "value1") == 0)) + { + field1Ok = true; + } + else if ((strcmp(si->Config->Metadata.List[i], "field2") == 0) + && (strcmp(si->Config->Metadata.List[i+1], "value2") == 0)) + { + field2Ok = true; + } + if (field1Ok && field2Ok) + break; + } + testCond(field1Ok && field2Ok); + jsStreamInfo_Destroy(si); + si = NULL; + + test("Free object: "); + // Check that NULL does not crash. + objStore_Destroy(NULL); + // Destroy object. + objStore_Destroy(obs); + obs = NULL; + testCond(true); + + test("Update (bad args): "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "TEST"; + s = js_UpdateObjectStore(NULL, js, &cfg); + if (s == NATS_INVALID_ARG) + s = js_UpdateObjectStore(&obs2, NULL, &cfg); + if (s == NATS_INVALID_ARG) + s = js_UpdateObjectStore(&obs2, js, NULL); + testCond((s == NATS_INVALID_ARG) && (obs2 == NULL)); + nats_clearLastError(); + + test("Update - no bucket name: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = NULL; + s = js_UpdateObjectStore(&obs2, js, &cfg); + testCond((s == NATS_INVALID_ARG) && (obs2 == NULL) + && (strstr(nats_GetLastError(NULL), obsErrInvalidStoreName) != NULL)); + nats_clearLastError(); + + test("Update - bad bucket name: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "This.is.not.a.valid.name!"; + s = js_UpdateObjectStore(&obs2, js, &cfg); + testCond((s == NATS_INVALID_ARG) && (obs2 == NULL) + && (strstr(nats_GetLastError(NULL), obsErrInvalidStoreName) != NULL)); + nats_clearLastError(); + + test("Update - not found: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "NOT_FOUND"; + s = js_UpdateObjectStore(&obs2, js, &cfg); + testCond((s == NATS_NOT_FOUND) && (obs2 == NULL) + && (strstr(nats_GetLastError(NULL), obsErrBucketNotFound) != NULL)); + nats_clearLastError(); + + test("Update: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "TEST"; + cfg.Description = "Testing Updated ObjectStore"; + cfg.Storage = js_MemoryStorage; + cfg.TTL = 10000; + cfg.MaxBytes = 512*1024*1024; + cfg.Metadata.List = (const char*[4]){"field1", "value1", "field2", "value2"}; + cfg.Metadata.Count = 2; + s = js_UpdateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + objStore_Destroy(obs); + obs = NULL; + + test("Get underlying stream info: "); + s = js_GetStreamInfo(&si, js, "OBJ_TEST", NULL, &jerr); + testCond((s == NATS_OK) && (si != NULL) && (jerr == 0)); + + test("Check properties: "); + testCond((strcmp(si->Config->Description, "Testing Updated ObjectStore") == 0) + && (si->Config->Storage == js_MemoryStorage) + && (si->Config->MaxAge == NATS_SECONDS_TO_NANOS(10)) + && (si->Config->MaxBytes == 512*1024*1024) + && (si->Config->Metadata.Count >= 2)); // There may be other meta data fields. + jsStreamInfo_Destroy(si); + si = NULL; + + test("Lookup with bad args: "); + s = js_ObjectStore(NULL, js, "TEST"); + if (s == NATS_INVALID_ARG) + s = js_ObjectStore(&obs, NULL, "TEST"); + if (s == NATS_INVALID_ARG) + s = js_ObjectStore(&obs, js, NULL); + if (s == NATS_INVALID_ARG) + s = js_ObjectStore(&obs, js, ""); + testCond((s == NATS_INVALID_ARG) && (obs == NULL)); + nats_clearLastError(); + + test("Lookup bad bucket name: "); + s = js_ObjectStore(&obs, js, "This.is.a.bad.name"); + testCond((s == NATS_INVALID_ARG) && (obs == NULL) + && (strstr(nats_GetLastError(NULL), obsErrInvalidStoreName) != NULL)); + nats_clearLastError(); + + test("Lookup not existing bucket: "); + s = js_ObjectStore(&obs, js, "BucketNotFound"); + testCond((s == NATS_NOT_FOUND) && (obs == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Lookup works: "); + s = js_ObjectStore(&obs, js, "TEST"); + testCond((s == NATS_OK) && (obs != NULL)); + objStore_Destroy(obs); + obs = NULL; + + test("Delete object store bad args: "); + s = js_DeleteObjectStore(NULL, "TEST"); + if (s == NATS_INVALID_ARG) + s = js_DeleteObjectStore(js, NULL); + if (s == NATS_INVALID_ARG) + s = js_DeleteObjectStore(js, ""); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Delete object store bad name: "); + s = js_DeleteObjectStore(js, "This.is.a.bad.name"); + testCond((s == NATS_INVALID_ARG) + && (strstr(nats_GetLastError(NULL), obsErrInvalidStoreName) != NULL)); + nats_clearLastError(); + + test("Delete object store not found: "); + s = js_DeleteObjectStore(js, "BucketNotFound"); + testCond((s == NATS_NOT_FOUND) && (nats_GetLastError(NULL) == NULL)); + + test("Delete object store: "); + s = js_DeleteObjectStore(js, "TEST"); + testCond(s == NATS_OK); + + test("Check if stream exists: "); + s = js_GetStreamInfo(&si, js, "OBJ_TEST", NULL, &jerr); + testCond((s == NATS_NOT_FOUND) && (jerr == JSStreamNotFoundErr)); + jsStreamInfo_Destroy(si); + + test("Check it is gone (bind should fail): "); + s = js_ObjectStore(&obs, js, "TEST"); + testCond((s == NATS_NOT_FOUND) && (obs == NULL)); + + test("Get names (none found): "); + s = js_ObjectStoreNames(&nl, js); + testCond((s == NATS_NOT_FOUND) && (nl == NULL) && + (nats_GetLastError(NULL) == NULL)); + + test("Get statuses (none found): "); + s = js_ObjectStoreStatuses(&sl, js); + testCond((s == NATS_NOT_FOUND) && (sl == NULL) && + (nats_GetLastError(NULL) == NULL)); + + test("Create several stores: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "Store1"; + cfg.Description = "Testing Store 1"; + cfg.Storage = js_MemoryStorage; + cfg.TTL = 10000; + cfg.MaxBytes = 1024*1024*1024; + cfg.Metadata.List = (const char*[4]){"field1", "value1", "field2", "value2"}; + cfg.Metadata.Count = 2; + s = js_CreateObjectStore(&obs, js, &cfg); + if (s == NATS_OK) + { + objStore_Destroy(obs); + obs = NULL; + + objStoreConfig_Init(&cfg); + cfg.Bucket = "Store2"; + cfg.Description = "Testing Store 2"; + cfg.Storage = js_FileStorage; + cfg.TTL = 20000; + cfg.MaxBytes = (int64_t)2*1024*1024*1024; + cfg.Metadata.List = (const char*[6]){"field3", "value3", "field4", "value4", "field5", "value5"}; + cfg.Metadata.Count = 3; + s = js_CreateObjectStore(&obs, js, &cfg); + } + testCond((s == NATS_OK) && (obs != NULL)); + objStore_Destroy(obs); + obs = NULL; + + test("Add stream that will match subject filter: "); + jsStreamConfig_Init(&scfg); + scfg.Name = "NotAnObjectStore"; + scfg.Subjects = (const char*[1]){"$O.something.C.>"}; + scfg.SubjectsLen = 1; + s = js_AddStream(NULL, js, &scfg, NULL, NULL); + IFOK(s, js_Publish(NULL, js, "$O.something.C.else", "hello", 5, NULL, NULL)); + testCond(s == NATS_OK); + + test("Get names (bad arg): "); + s = js_ObjectStoreNames(NULL, NULL); + if (s == NATS_INVALID_ARG) + s = js_ObjectStoreNames(NULL, js); + if (s == NATS_INVALID_ARG) + s = js_ObjectStoreNames(&nl, NULL); + testCond ((s == NATS_INVALID_ARG) && (nl == NULL)); + nats_clearLastError(); + + test("Get names: "); + s = js_ObjectStoreNames(&nl, js); + testCond((s == NATS_OK) && (nl != NULL) && (nl->List != NULL) && (nl->Count == 2)); + + test("Check names: "); + s = NATS_ERR; + if ((strcmp(nl->List[0], "Store1") == 0) && (strcmp(nl->List[1], "Store2") == 0)) + s = NATS_OK; + else if ((strcmp(nl->List[0], "Store2") == 0) && (strcmp(nl->List[1], "Store1") == 0)) + s = NATS_OK; + testCond(s == NATS_OK); + + test("Destroy list: "); + // Check that does not crash. + objStoreNamesList_Destroy(NULL); + // Destroy actual list. + objStoreNamesList_Destroy(nl); + nl = NULL; + testCond(true); + + test("Get statuses (bad arg): "); + s = js_ObjectStoreStatuses(NULL, NULL); + if (s == NATS_INVALID_ARG) + s = js_ObjectStoreStatuses(NULL, js); + if (s == NATS_INVALID_ARG) + s = js_ObjectStoreStatuses(&sl, NULL); + testCond ((s == NATS_INVALID_ARG) && (sl == NULL)); + nats_clearLastError(); + + test("Get statuses: "); + s = js_ObjectStoreStatuses(&sl, js); + testCond((s == NATS_OK) && (sl != NULL) && (sl->List != NULL) && (sl->Count == 2)); + + test("Check status: "); + for (i=0; (s == NATS_OK) && (i<2); i++) + { + objStoreStatus *oss = sl->List[i]; + if (strcmp(oss->Bucket, "Store1") == 0) + { + if ((strcmp(oss->Description, "Testing Store 1") != 0) + || (oss->Storage != js_MemoryStorage) + || (oss->TTL != 10000) + || (oss->Metadata.List == NULL) + // there may be one more for "_nats.req.level", so at least 2. + || (oss->Metadata.Count < 2)) + { + s = NATS_ERR; + } + } + else if (strcmp(oss->Bucket, "Store2") == 0) + { + if ((strcmp(oss->Description, "Testing Store 2") != 0) + || (oss->Storage != js_FileStorage) + || (oss->TTL != 20000) + || (oss->Metadata.List == NULL) + // there may be one more for "_nats.req.level", so at least 3. + || (oss->Metadata.Count < 3)) + { + s = NATS_ERR; + } + } + else + s = NATS_ERR; + } + testCond(s == NATS_OK); + + test("Destroy list: "); + // Check that does not crash. + objStoreStatusesList_Destroy(NULL); + // Destroy actual list. + objStoreStatusesList_Destroy(sl); + sl = NULL; + testCond(true); + + test("Delete object stores: "); + s = js_DeleteObjectStore(js, "Store1"); + IFOK(s, js_DeleteObjectStore(js, "Store2")); + testCond(s == NATS_OK); + + test("Get names with stream matching filter: "); + s = js_ObjectStoreNames(&nl, js); + testCond((s == NATS_NOT_FOUND) && (nl == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Get statuses with stream matching filter: "); + s = js_ObjectStoreStatuses(&sl, js); + testCond((s == NATS_NOT_FOUND) && (sl == NULL) && (nats_GetLastError(NULL) == NULL)); + + JS_TEARDOWN; +} + +void test_ObjectStore_PutAndGet(void) +{ + natsStatus s; + objStore *obs = NULL; + objStoreInfo *info = NULL; + char *echunkSubj = NULL; + natsMsg *msg = NULL; + natsBuffer *buf = NULL; + natsBuffer *buf2 = NULL; + const char *esha = NULL; + char *str = NULL; + void *data = NULL; + int len = 0; + bool done = false; + objStoreGet *get = NULL; + const char *fname = NULL; + objStorePut *put = NULL; + char *chunkSubj = NULL; +#ifndef NATS_HAS_TLS + const char *nosha = "SHA-256=bm90IHN1cHBvcnRlZA=="; +#endif + objStoreConfig cfg; + objStoreMeta meta; + + JS_SETUP(2, 10, 0); + + test("Create ObjectStore: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "TEST"; + cfg.Description = "Testing ObjectStore"; + cfg.Storage = js_FileStorage; + cfg.TTL = 10000; + cfg.MaxBytes = 1024*1024*1024; + cfg.Metadata.List = (const char*[4]){"field1", "value1", "field2", "value2"}; + cfg.Metadata.Count = 2; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Put string (bad args): "); + s = objStore_PutString(&info, NULL, "putstring", "test string"); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("Put string (bad name): "); + s = objStore_PutString(&info, obs, NULL, "test string"); + if (s == NATS_INVALID_ARG) + s = objStore_PutString(&info, obs, "", "test string"); + testCond((s == NATS_INVALID_ARG) && (info == NULL) + && (strstr(nats_GetLastError(NULL), obsErrBadObjectMeta) != NULL)); + nats_clearLastError(); + +#ifndef NATS_HAS_TLS + test("Put errors on no tls: "); + s = objStore_PutString(&info, obs, "foobar", "should error out"); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL) && + (strstr(nats_GetLastError(NULL), NO_SSL_ERR) != NULL)); + nats_clearLastError(); + + // From now on, simply avoid errors due to hash256. + nats_hashNoErrorOnNoSSL(true); + esha = nosha; +#endif + + test("Put string (check names): "); + s = objStore_PutString(NULL, obs, "BLOB.txt", "A"); + IFOK(s, objStore_PutString(NULL, obs, "foo bar", "B")); + IFOK(s, objStore_PutString(NULL, obs, ".*<>:\"/\\|?&", "C")); + testCond(s == NATS_OK); + + test("Put string (get info back): "); + s = objStore_PutString(&info, obs, "foo", "DEF"); + testCond((s == NATS_OK) && (info != NULL)); + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=lnxaW34vu-MICgxc7-p8J5VwsWroRlUlU4vDsRUmekU="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "foo") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 3) && + (info->ModTime > 0) && + (info->Chunks == 1) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + test("Copy info's subject: "); + s = (nats_asprintf(&echunkSubj, "$O.TEST.C.%s", info->NUID) >= 0 ? NATS_OK : NATS_ERR); + testCond(s == NATS_OK); + + objStoreInfo_Destroy(info); + info = NULL; + + test("Get string (bad args): "); + s = objStore_GetString(NULL, obs, "foo", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetString(&str, NULL, "foo", NULL); + testCond((s == NATS_INVALID_ARG) && (str == NULL)); + nats_clearLastError(); + + test("Get string (bad name): "); + s = objStore_GetString(&str, obs, "", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetString(&str, obs, NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (str == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Get string (not found): "); + s = objStore_GetString(&str, obs, "notfound", NULL); + testCond((s == NATS_NOT_FOUND) && (str == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Get string: "); + s = objStore_GetString(&str, obs, "foo", NULL); + testCond((s == NATS_OK) && (str != NULL) && (strcmp(str, "DEF") == 0)); + free(str); + str = NULL; + + test("Get info msg: "); + s = js_GetLastMsg(&msg, js, "OBJ_TEST", echunkSubj, NULL, NULL); + testCond((s == NATS_OK) && (msg != NULL) && + (strstr(natsMsg_GetData(msg), "DEF") != NULL)); + natsMsg_Destroy(msg); + msg = NULL; + + test("Put other string with same name: "); + s = objStore_PutString(&info, obs, "foo", "GHIJKLMNOPQRSTUVWXYZ"); + testCond((s == NATS_OK) && (info != NULL)); + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=QzNBausNa1iF0xPksPiDUXy6ppkd9YvDanB1r9wJW-A="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "foo") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 20) && + (info->ModTime > 0) && + (info->Chunks == 1) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + objStoreInfo_Destroy(info); + info = NULL; + + test("Check old info msg removed: "); + s = js_GetLastMsg(&msg, js, "OBJ_TEST", echunkSubj, NULL, NULL); + testCond((s == NATS_NOT_FOUND) && (msg == NULL)); + nats_clearLastError(); + + free(echunkSubj); + echunkSubj = NULL; + + test("Put with empty string: "); + s = objStore_PutString(NULL, obs, "emptystring", ""); + testCond(s == NATS_OK); + + test("Get empty string: "); + s = objStore_GetString(&str, obs, "emptystring", NULL); + testCond((s == NATS_OK) && (str == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Put with NULL string: "); + s = objStore_PutString(NULL, obs, "nullstring", NULL); + testCond(s == NATS_OK); + + test("Get NULL string: "); + s = objStore_GetString(&str, obs, "nullstring", NULL); + testCond((s == NATS_OK) && (str == NULL) && (nats_GetLastError(NULL) == NULL)); + + // Put Bytes + + test("Put bytes (bad args): "); + s = objStore_PutBytes(&info, NULL, "put bytes", (const void*) "bytes", 5); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("Put bytes (bad name): "); + s = objStore_PutBytes(&info, obs, NULL, (const void*) "bytes", 5); + if (s == NATS_INVALID_ARG) + s = objStore_PutBytes(&info, obs, "", (const void*) "bytes", 5); + testCond((s == NATS_INVALID_ARG) && (info == NULL) + && (strstr(nats_GetLastError(NULL), obsErrBadObjectMeta) != NULL)); + nats_clearLastError(); + + test("Put bytes (check names): "); + s = objStore_PutBytes(NULL, obs, "BLOBBYTES.txt", (const void*) "bytes", 5); + IFOK(s, objStore_PutBytes(NULL, obs, "foo bar baz", (const void*) "Bytes", 5)); + IFOK(s, objStore_PutBytes(NULL, obs, "bytes.*<>:\"/\\|?&", (const void*) "BYTES", 5)); + testCond(s == NATS_OK); + + test("Put bytes (get info back): "); + s = objStore_PutBytes(&info, obs, "foo", (const void*) "some bytes", 10); + testCond((s == NATS_OK) && (info != NULL)); + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=DSLNzBDm0Enb4a9RI9UIc_38Gk9YMG5Yy2JBvpRyAU0="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "foo") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 10) && + (info->ModTime > 0) && + (info->Chunks == 1) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + objStoreInfo_Destroy(info); + info = NULL; + + test("Get bytes (bad args): "); + s = objStore_GetBytes(NULL, &len, obs, "foo", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetBytes(&data, NULL, obs, "foo", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetBytes(&data, &len, NULL, "foo", NULL); + testCond((s == NATS_INVALID_ARG) && (data == NULL) && (len == 0)); + nats_clearLastError(); + + test("Get bytes (bad name): "); + s = objStore_GetBytes(&data, &len, obs, "", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetBytes(&data, &len, obs, NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (data == NULL) && (len == 0) && + (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Get bytes (not found): "); + s = objStore_GetBytes(&data, &len, obs, "notfound", NULL); + testCond((s == NATS_NOT_FOUND) && (data == NULL) && (len == 0) && (nats_GetLastError(NULL) == NULL)); + + test("Get string: "); + s = objStore_GetBytes(&data, &len, obs, "foo", NULL); + testCond((s == NATS_OK) && (data != NULL) && (len == 10) && + (memcmp((const void*) data, (const void*) "some bytes", 10) == 0)); + free(data); + data = NULL; + len = 0; + + test("Put bytes (0 length): "); + s = objStore_PutBytes(NULL, obs, "nullbytes", NULL, 0); + testCond((s == NATS_OK) && (info == NULL)); + + test("Get bytes (0 length): "); + s = objStore_GetBytes(&data, &len, obs, "nullbytes", NULL); + testCond((s == NATS_OK) && (data == NULL) && (len == 0) && (nats_GetLastError(NULL) == NULL)); + + test("Put file (bad args): "); + s = objStore_PutFile(&info, NULL, "test.txt"); + if (s == NATS_INVALID_ARG) + s = objStore_PutFile(&info, obs, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_PutFile(&info, obs, ""); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("Put file (not found): "); + s = objStore_PutFile(&info, obs, "test.txt"); + testCond((s == NATS_ERR) && (info == NULL) && + (strstr(nats_GetLastError(NULL), "error opening") != NULL)); + nats_clearLastError(); + + test("Put file (get info back): "); + s = objStore_PutFile(&info, obs, "list_stan.txt"); + testCond((s == NATS_OK) && (info != NULL)); + + test("Read same file for checks: "); + s = nats_ReadFile(&buf, 1024, "list_stan.txt"); + testCond(s == NATS_OK); + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=jTPSL4eQLmyLeId-rrZW5-HYikPO8s1k3YvGDd0kg4M="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "list_stan.txt") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == (uint64_t)(natsBuf_Len(buf)-1)) && + (info->ModTime > 0) && + (info->Chunks == 1) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + test("Check content: "); + s = (nats_asprintf(&echunkSubj, "$O.TEST.C.%s", info->NUID) >= 0 ? NATS_OK : NATS_ERR); + IFOK(s, js_GetLastMsg(&msg, js, "OBJ_TEST", echunkSubj, NULL, NULL)); + testCond((s == NATS_OK) && (msg != NULL) && + (strncmp(natsMsg_GetData(msg), natsBuf_Data(buf), natsBuf_Len(buf)) == 0)); + free(echunkSubj); + natsMsg_Destroy(msg); + msg = NULL; + objStoreInfo_Destroy(info); + info = NULL; + + test("Get file (bad args): "); + fname = "objstore_getfile.txt"; + s = objStore_GetFile(NULL, "list_stan.txt", fname, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetFile(obs, "", fname, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetFile(obs, NULL, fname, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetFile(obs, "list_stan.txt", "", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetFile(obs, "list_stan.txt", NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (fopen(fname, "r") == NULL)); + nats_clearLastError(); + + test("Get file (not found): "); + s = objStore_GetFile(obs, "notfound", fname, NULL); + testCond((s == NATS_NOT_FOUND) && (fopen(fname, "r") == NULL) && + (nats_GetLastError(NULL) == NULL)); + + test("Get file: "); + s = objStore_GetFile(obs, "list_stan.txt", fname, NULL); + testCond(s == NATS_OK); + + test("Read generated file: "); + s = nats_ReadFile(&buf2, 1024, fname); + testCond((s == NATS_OK) && (buf2 != NULL) && (natsBuf_Len(buf2) == natsBuf_Len(buf))); + + test("Compare files: "); + s = (memcmp((const void*) natsBuf_Data(buf), (const void*) natsBuf_Data(buf2), + natsBuf_Len(buf)) == 0 ? NATS_OK : NATS_ERR); + testCond(s == NATS_OK); + + remove(fname); + natsBuf_Destroy(buf); + buf = NULL; + natsBuf_Destroy(buf2); + buf2 = NULL; + + test("Check meta init: "); + s = objStoreMeta_Init(NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Put (bad args): "); + objStoreMeta_Init(&meta); + s = objStore_Put(NULL, obs, &meta); + if (s == NATS_INVALID_ARG) + s = objStore_Put(&put, NULL, &meta); + if (s == NATS_INVALID_ARG) + s = objStore_Put(&put, obs, NULL); + testCond((s == NATS_INVALID_ARG) && (put == NULL)); + nats_clearLastError(); + + test("Put (bad name): "); + meta.Name = NULL; + s = objStore_Put(&put, obs, &meta); + if (s == NATS_INVALID_ARG) + { + meta.Name = ""; + s = objStore_Put(&put, obs, &meta); + } + testCond((s == NATS_INVALID_ARG) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrBadObjectMeta) != NULL)); + nats_clearLastError(); + + test("Put (get put back): "); + meta.Name = "test put"; + s = objStore_Put(&put, obs, &meta); + testCond((s == NATS_OK) && (put != NULL)); + + test("Put add (bad args): "); + s = objStorePut_Add(NULL, "test", 4); + if (s == NATS_INVALID_ARG) + s = objStorePut_Add(put, NULL, 4); + if (s == NATS_INVALID_ARG) + s = objStorePut_Add(put, "test", 0); + if (s == NATS_INVALID_ARG) + s = objStorePut_Add(put, "test", -10); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Add two chunks: "); + s = objStorePut_Add(put, "this is the first chunk", 23); + IFOK(s, objStorePut_Add(put, "this is the second chunk", 24)); + IFOK(s, objStorePut_Complete(&info, put, 1000)); + testCond((s == NATS_OK) && (info != NULL)); + + test("Destroy put: "); + // should not crash + objStorePut_Destroy(NULL); + objStorePut_Destroy(put); + testCond(true); + put = NULL; + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=3UmPFsVY1hIZIKngP09wv2IaKNIV6s-Y1VjaGvhzZrk="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "test put") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 47) && + (info->ModTime > 0) && + (info->Chunks == 2) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + objStoreInfo_Destroy(info); + info = NULL; + + test("Get (bad args): "); + s = objStore_Get(NULL, obs, "foo", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_Get(&get, NULL,"foo", NULL); + testCond((s == NATS_INVALID_ARG) && (get == NULL)); + nats_clearLastError(); + + test("Get (bad name): "); + s = objStore_Get(&get, obs, "", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_Get(&get, obs, NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (get == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Get (result back): "); + s = objStore_Get(&get, obs, "test put", NULL); + testCond((s == NATS_OK) && (get != NULL)); + + test("Result get info (bad args): "); + s = objStoreGet_Info(NULL, get); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Info((const objStoreInfo**) &info, NULL); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("Result get info: "); + s = objStoreGet_Info((const objStoreInfo**) &info, get); + testCond((s == NATS_OK) && (info != NULL)); + + test("Check info: "); + testCond((strcmp(info->Meta.Name, "test put") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 47) && + (info->ModTime > 0) && + (info->Chunks == 2) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + test("Read (bad args): "); + s = objStoreGet_Read(NULL, &data, &len, get, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Read(&done, NULL, &len, get, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Read(&done, &data, NULL, get, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Read(&done, &data, &len, NULL, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Read(&done, &data, &len, get, 0); + if (s == NATS_INVALID_ARG) + s = objStoreGet_Read(&done, &data, &len, get, -10); + testCond((s == NATS_INVALID_ARG) && (!done) && (data == NULL) && (len == 0)); + nats_clearLastError(); + + test("Read (first chunk): "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_OK) && (!done) && (data != NULL) && (len == 23) && + (memcmp((const void*) data, (const void*) "this is the first chunk", len) == 0)); + free(data); + data = NULL; + len = 0; + + test("Read (second chunk): "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_OK) && done && (data != NULL) && (len == 24) && + (memcmp((const void*) data, (const void*) "this is the second chunk", len) == 0)); + free(data); + data = NULL; + len = 0; + done = false; + + test("Read (no more): "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (!done) && (data == NULL) && (len == 0) && + (strstr(nats_GetLastError(NULL), obsErrReadComplete) != NULL)); + nats_clearLastError(); + + test("Destroy result: "); + // Should not crash. + objStoreGet_Destroy(NULL); + objStoreGet_Destroy(get); + testCond(true); + get = NULL; + + test("Get with null bytes: "); + s = objStore_Get(&get, obs, "nullbytes", NULL); + testCond((s == NATS_OK) && (get != NULL)); + + test("Read: "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_OK) && done && (data == NULL) && (len == 0)); + + done = false; + test("Read (no more): "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (!done) && (data == NULL) && (len == 0) && + (strstr(nats_GetLastError(NULL), obsErrReadComplete) != NULL)); + nats_clearLastError(); + + objStoreGet_Destroy(get); + get = NULL; + + test("Get with null bytes: "); + s = objStore_Get(&get, obs, "nullbytes", NULL); + testCond((s == NATS_OK) && (get != NULL)); + + test("Read all: "); + s = objStoreGet_ReadAll(&data, &len, get, 1000); + testCond((s == NATS_OK) && (data == NULL) && (len == 0)); + + test("Read (no more): "); + s = objStoreGet_ReadAll(&data, &len, get, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (data == NULL) && (len == 0) && + (strstr(nats_GetLastError(NULL), obsErrReadComplete) != NULL)); + nats_clearLastError(); + + objStoreGet_Destroy(get); + get = NULL; + + test("Put (small chunk size): "); + meta.Opts.ChunkSize = 5; + meta.Name = "test put 2"; + s = objStore_Put(&put, obs, &meta); + IFOK(s, objStorePut_Add(put, "abcdefghijklm", 13)); + IFOK(s, objStorePut_Complete(&info, put, 1000)); + testCond((s == NATS_OK) && (info != NULL)); + + objStorePut_Destroy(put); + put = NULL; + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=_xAwTxryNgbt4eLYq83JTCKQR6YUWNgJ2LvVPt4fZZg="; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "test put 2") == 0) && + (info->Meta.Opts.ChunkSize == 5) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 13) && + (info->ModTime > 0) && + (info->Chunks == 3) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + objStoreInfo_Destroy(info); + info = NULL; + + test("Get (result back): "); + s = objStore_Get(&get, obs, "test put 2", NULL); + testCond((s == NATS_OK) && (get != NULL)); + + done = false; + test("Read (first chunk): "); + s = objStoreGet_Read(&done, &data, &len, get, 1000); + testCond((s == NATS_OK) && (!done) && (data != NULL) && (len == 5) && + (memcmp((const void*) data, (const void*) "abcde", len) == 0)); + free(data); + data = NULL; + len = 0; + + test("Read all for the rest: "); + s = objStoreGet_ReadAll(&data, &len, get, 1000); + testCond((s == NATS_OK) && (data != NULL) && (len == 8) && + (memcmp((const void*) data, (const void*) "fghijklm", len) == 0)); + free(data); + data = NULL; + len = 0; + + test("Read all (no more): "); + s = objStoreGet_ReadAll(&data, &len, get, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (data == NULL) && (len == 0) && + (strstr(nats_GetLastError(NULL), obsErrReadComplete) != NULL)); + nats_clearLastError(); + + objStoreGet_Destroy(get); + get = NULL; + + meta.Name = "test put 3"; + test("Put with error: "); + s = objStore_Put(&put, obs, &meta); + IFOK(s, objStorePut_Add(put, "some", 4)); + IFOK(s, objStorePut_Add(put, "data", 4)); + // Wait for those to be written. + IFOK(s, js_PublishAsyncComplete(obs->js, NULL)); + if (s == NATS_OK) + { + // Save the subject to get if messages have been persisted. + chunkSubj = strdup(put->chunkSubj); + if (chunkSubj == NULL) + s = NATS_NO_MEMORY; + } + testCond(s == NATS_OK); + // If we destroy before completing the Add, we should get the chunks purged. + objStorePut_Destroy(put); + put = NULL; + + // Wait a bit to make sure purge has time to be done. + nats_Sleep(250); + + test("Check info: "); + s = js_GetLastMsg(&msg, js, "OBJ_TEST", chunkSubj, NULL, NULL); + testCond((s == NATS_NOT_FOUND) && (msg == NULL)); + nats_clearLastError(); + free(chunkSubj); + chunkSubj = NULL; + + test("Put with link error: "); + // Normally, users don't set Link directly (done through Add[Bucket]Link() + // that generates a objStoreInfo, so would not be passed to a Put() call, + // but simulate that the Link pointer is not NULL to check that we are + // properly rejecting. + s = NATS_OK; + meta.Name = "test put 4"; + meta.Opts.Link = (objStoreLink*) calloc(1, sizeof(objStoreLink)); + if (meta.Opts.Link == NULL) + s = NATS_NO_MEMORY; + else + s = objStore_Put(&put, obs, &meta); + testCond((s == NATS_INVALID_ARG) && (put == NULL) && + (strstr(nats_GetLastError(NULL), obsErrLinkNotAllowed) != NULL)); + nats_clearLastError(); + free(meta.Opts.Link); + meta.Opts.Link = NULL; + objStore_Destroy(obs); + obs = NULL; + + test("Create ObjectStore with max bytes low: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "TEST2"; + cfg.Description = "Testing ObjectStore Errors"; + cfg.Storage = js_FileStorage; + cfg.MaxBytes = 100; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Start a put: ") + objStoreMeta_Init(&meta); + meta.Name = "test put maxbytes err"; + s = objStore_Put(&put, obs, &meta); + if (s == NATS_OK) + { + // Save the subject to get if messages have been persisted. + chunkSubj = strdup(put->chunkSubj); + if (chunkSubj == NULL) + s = NATS_NO_MEMORY; + } + testCond((s == NATS_OK) && (put != NULL)); + + test("Add some bytes: "); + s = objStorePut_Add(put, "abcdefghijklmnopqrstuvwxyz", 26); + testCond(s == NATS_OK); + + test("Check pub complete: "); + s = js_PublishAsyncComplete(put->pubJS, NULL); + testCond(s == NATS_OK); + + test("Check chunk persisted: "); + s = js_GetLastMsg(&msg, js, "OBJ_TEST2", chunkSubj, NULL, NULL); + testCond((s == NATS_OK) && (msg != NULL) && (natsMsg_GetDataLength(msg) == 26)); + natsMsg_Destroy(msg); + msg = NULL; + + test("Add more bytes: "); + s = objStorePut_Add(put, "abcdefghijklmnopqrstuvwxyz", 26); + testCond(s == NATS_OK); + + test("Complete should return error: "); + s = objStorePut_Complete(&info, put, 1000); + testCond((s == NATS_ERR) && (info == NULL) && + (strstr(nats_GetLastError(NULL), "maximum") != NULL)); + nats_clearLastError(); + + test("Check chunk purged: "); + s = js_GetLastMsg(&msg, js, "OBJ_TEST2", chunkSubj, NULL, NULL); + testCond((s == NATS_NOT_FOUND) && (msg == NULL)); + nats_clearLastError(); + + objStorePut_Destroy(put); + objStore_Destroy(obs); + free(chunkSubj); + + JS_TEARDOWN; +} + +void test_ObjectStore_StoreMgt(void) +{ + natsStatus s; + objStore *obs = NULL; + objStoreInfo *info = NULL; + objStoreStatus *sts = NULL; + objStorePut *put = NULL; + natsHeader *hdr = NULL; + const char *esha = NULL; + const char **keys= NULL; + const char **vals= NULL; + int count = 0; +#ifndef NATS_HAS_TLS + const char *nosha= "SHA-256=bm90IHN1cHBvcnRlZA=="; +#endif + objStoreConfig cfg; + objStoreMeta meta; + + JS_SETUP(2, 10, 0); + + test("Create ObjectStore: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "TEST"; + cfg.Description = "Testing ObjectStore"; + cfg.Storage = js_FileStorage; + cfg.TTL = 10000; + cfg.MaxBytes = 1024*1024*1024; + cfg.Metadata.List = (const char*[4]){"field1", "value1", "field2", "value2"}; + cfg.Metadata.Count = 2; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("GetInfo (bad args): "); + s = objStore_GetInfo(NULL, obs, "testinfo", NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetInfo(&info, NULL, "testinfo", NULL); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("GetInfo (bad name): "); + s = objStore_GetInfo(&info, obs, NULL, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_GetInfo(&info, obs, "", NULL); + testCond((s == NATS_INVALID_ARG) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("GetInfo (not found): "); + s = objStore_GetInfo(&info, obs, "notfound", NULL); + testCond((s == NATS_NOT_FOUND) && (info == NULL) && (nats_GetLastError(NULL) == NULL)); + +#ifndef NATS_HAS_TLS + nats_hashNoErrorOnNoSSL(true); +#endif + test("GetInfo: "); + s = objStore_PutString(NULL, obs, "test", "this is a test string"); + IFOK(s, objStore_GetInfo(&info, obs, "test", NULL)); + testCond((s == NATS_OK) && (info != NULL)); + +#if defined(NATS_HAS_TLS) + esha = "SHA-256=9ndFGdHHozie8yfpwEdmuZnbjN-4XRNGxHHuhtZYhbw="; +#else + esha = nosha; +#endif + test("Check info: "); + testCond((strcmp(info->Meta.Name, "test") == 0) && + (info->Meta.Opts.ChunkSize == obsDefaultChunkSize) && + (strcmp(info->Bucket, "TEST") == 0) && + (!nats_IsStringEmpty(info->NUID)) && + (info->Size == 21) && + (info->ModTime > 0) && + (info->Chunks == 1) && + (!nats_IsStringEmpty(info->Digest)) && + (strcmp(info->Digest, esha) == 0)); + + objStoreInfo_Destroy(info); + info = NULL; + + objStore_Destroy(obs); + obs = NULL; + + test("Create store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "META-TEST"; + cfg.Metadata.List = (const char*[]){"foo", "bar", "baz", "boo"}; + cfg.Metadata.Count = 2; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Status (bad args): "); + s = objStore_Status(NULL, obs); + if (s == NATS_INVALID_ARG) + s = objStore_Status(&sts, NULL); + testCond((s == NATS_INVALID_ARG) && (sts == NULL)); + nats_clearLastError(); + + test("Status: "); + s = objStore_Status(&sts, obs); + testCond((s == NATS_OK) && (sts != NULL)); + + test("Check meta data: "); + int ok = 0; + int i = 0; + for (i = 0; i < 2*sts->Metadata.Count; ) + { + if ((strcmp(sts->Metadata.List[i], "foo") == 0) && + (strcmp(sts->Metadata.List[i+1], "bar") == 0)) + { + ok++; + } else if ((strcmp(sts->Metadata.List[i], "baz") == 0) && + (strcmp(sts->Metadata.List[i+1], "boo") == 0)) + { + ok++; + } + i += 2; + } + testCond(ok == 2); + + test("Destroy status: "); + objStoreStatus_Destroy(NULL); + objStoreStatus_Destroy(sts); + sts = NULL; + testCond(true); + + test("Simple put with no meta: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + testCond(s == NATS_OK); + + test("Put with meta data: "); + objStoreMeta_Init(&meta); + meta.Name = "C"; + meta.Metadata.List = (const char*[]){"name", "C", "description", "descC"}; + meta.Metadata.Count = 2; + s = objStore_Put(&put, obs, &meta); + IFOK(s, objStorePut_Add(put, "CCC", 3)); + IFOK(s, objStorePut_Complete(&info, put, 1000)); + testCond((s == NATS_OK) && (info != NULL)); + + objStorePut_Destroy(put); + put = NULL; + + test("Check info: "); + ok = 0; + for (i = 0; i < 2*info->Meta.Metadata.Count; ) + { + if ((strcmp(info->Meta.Metadata.List[i], "name") == 0) && + (strcmp(info->Meta.Metadata.List[i+1], "C") == 0)) + { + ok++; + } else if ((strcmp(info->Meta.Metadata.List[i], "description") == 0) && + (strcmp(info->Meta.Metadata.List[i+1], "descC") == 0)) + { + ok++; + } + i += 2; + } + testCond(ok == 2); + objStoreInfo_Destroy(info); + info = NULL; + + test("Update meta (bad args): "); + s = objStore_UpdateMeta(NULL, "name", &meta); + if (s == NATS_INVALID_ARG) + s = objStore_UpdateMeta(obs, "name", NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Update meta (bad name): "); + s = objStore_UpdateMeta(obs, "", &meta); + if (s == NATS_INVALID_ARG) + s = objStore_UpdateMeta(obs, NULL, &meta); + testCond((s == NATS_INVALID_ARG) && (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Simple meta update: "); + objStoreMeta_Init(&meta); + meta.Name = "A"; + meta.Description = "descA"; + meta.Metadata.List = (const char*[]){"description", "updated desc", "version", "0.1"}; + meta.Metadata.Count = 2; + s = natsHeader_New(&hdr); + IFOK(s, natsHeader_Set(hdr, "color", "blue")); + IFOK(s, natsHeader_Add(hdr, "color", "")); + IFOK(s, natsHeader_Add(hdr, "color", "red")); + meta.Headers = hdr; + IFOK(s, objStore_UpdateMeta(obs, "A", &meta)); + testCond(s == NATS_OK); + + test("Check info: "); + s = objStore_GetInfo(&info, obs, "A", NULL); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "A") == 0) && + (strcmp(info->Meta.Description, "descA") == 0) && + (info->Meta.Metadata.Count >= 2) && + (info->Meta.Headers != NULL) && + (natsHeader_KeysCount(info->Meta.Headers) == 1)); + + test("Check header keys: "); + s = natsHeader_Keys(info->Meta.Headers, &keys, &count); + testCond((s == NATS_OK) && (count == 1) && (keys != NULL) && + (strcmp(keys[0], "color") == 0)); + free(keys); + keys = NULL; + count = 0; + + test("Check header values: "); + s = natsHeader_Values(info->Meta.Headers, "color", &vals, &count); + testCond((s == NATS_OK) && (count == 3) && (vals != NULL) && + (strcmp(vals[0], "blue") == 0) && + ((vals[1] != NULL) && (vals[1][0] == '\0')) && + (strcmp(vals[2], "red") == 0)); + free(vals); + vals = NULL; + count = 0; + + test("Check meta: "); + ok = 0; + for (i = 0; i < 2*info->Meta.Metadata.Count; ) + { + if ((strcmp(info->Meta.Metadata.List[i], "description") == 0) && + (strcmp(info->Meta.Metadata.List[i+1], "updated desc") == 0)) + { + ok++; + } else if ((strcmp(info->Meta.Metadata.List[i], "version") == 0) && + (strcmp(info->Meta.Metadata.List[i+1], "0.1") == 0)) + { + ok++; + } + i += 2; + } + testCond(ok == 2); + objStoreInfo_Destroy(info); + info = NULL; + + test("Update that changes name and some data: "); + objStoreMeta_Init(&meta); + meta.Name = "B"; + meta.Description = "descB"; + // Reuse header object. + s = natsHeader_Set(hdr, "color", "black"); + IFOK(s, natsHeader_Set(hdr, "foo", "bar")); + meta.Headers = hdr; + s = objStore_UpdateMeta(obs, "A", &meta); + testCond(s == NATS_OK); + + test("Check info for A: "); + s = objStore_GetInfo(&info, obs, "A", NULL); + testCond((s == NATS_NOT_FOUND) && (info == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Check info for B: "); + s = objStore_GetInfo(&info, obs, "B", NULL); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "B") == 0) && + (strcmp(info->Meta.Description, "descB") == 0) && (info->Meta.Metadata.Count == 0) && + (info->Meta.Headers != NULL) && (natsHeader_KeysCount(info->Meta.Headers) == 2)); + + test("Check header keys: "); + s = natsHeader_Keys(info->Meta.Headers, &keys, &count); + testCond((s == NATS_OK) && (count == 2) && (keys != NULL) && + (((strcmp(keys[0], "color") == 0) && (strcmp(keys[1], "foo") == 0)) || + (((strcmp(keys[0], "foo") == 0) && (strcmp(keys[1], "color") == 0))))); + free(keys); + keys = NULL; + count = 0; + + test("Check header values (color): "); + s = natsHeader_Values(info->Meta.Headers, "color", &vals, &count); + testCond((s == NATS_OK) && (count == 1) && (vals != NULL) && (strcmp(vals[0], "black") == 0)); + free(vals); + vals = NULL; + count = 0; + + test("Check header values (foo): "); + s = natsHeader_Values(info->Meta.Headers, "foo", &vals, &count); + testCond((s == NATS_OK) && (count == 1) && (vals != NULL) && (strcmp(vals[0], "bar") == 0)); + free(vals); + vals = NULL; + count = 0; + natsHeader_Destroy(hdr); + hdr = NULL; + + objStoreInfo_Destroy(info); + info = NULL; + + test("Change meta name to existing object: "); + objStoreMeta_Init(&meta); + meta.Name = "C"; + s = objStore_UpdateMeta(obs, "B", &meta); + testCond((s == NATS_ILLEGAL_STATE) && (strstr(nats_GetLastError(NULL), obsErrObjectAlreadyExists) != NULL)); + nats_clearLastError(); + + test("Delete (bad args): "); + s = objStore_Delete(NULL, "C"); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Delete (bad name): "); + s = objStore_Delete(obs, ""); + if (s == NATS_INVALID_ARG) + s = objStore_Delete(obs, NULL); + testCond((s == NATS_INVALID_ARG) && (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Delete not found: "); + s = objStore_Delete(obs, "notfound"); + testCond((s == NATS_NOT_FOUND) && (nats_GetLastError(NULL) == NULL)); + + test("Delete C object: "); + s = objStore_Delete(obs, "C"); + testCond(s == NATS_OK); + + test("Update should be ok now: "); + s = objStore_UpdateMeta(obs, "B", &meta); + testCond(s == NATS_OK); + + test("Delete of deleted object ok: "); + s = objStore_Delete(obs, "C"); + testCond(s == NATS_OK); + + test("Update meta of deleted object: "); + s = objStore_UpdateMeta(obs, "C", &meta); + testCond((s == NATS_ILLEGAL_STATE) && (strstr(nats_GetLastError(NULL), obsErrUpdateMetaDelete) != NULL)); + nats_clearLastError(); + + test("Update of object not found: "); + s = objStore_UpdateMeta(obs, "X", &meta); + testCond((s == NATS_ILLEGAL_STATE) && (strstr(nats_GetLastError(NULL), obsErrUpdateMetaDelete) != NULL)); + nats_clearLastError(); + + objStore_Destroy(obs); + obs = NULL; + + test("Create object store: "); + objStoreConfig_Init(&cfg); + cfg.Bucket = "SEAL"; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Put string: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + testCond(s == NATS_OK); + + test("Seal (bad args): "); + s = objStore_Seal(NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Seal: "); + s = objStore_Seal(obs); + testCond(s == NATS_OK); + + test("Put should fail: "); + s = objStore_PutString(NULL, obs, "B", "BBB"); + testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "sealed") != NULL)); + nats_clearLastError(); + + test("Delete underlying stream: "); + s = js_DeleteStream(js, "OBJ_SEAL", NULL, NULL); + testCond(s == NATS_OK); + + test("Seal should fail: "); + s = objStore_Seal(obs); + testCond(s == NATS_NOT_FOUND); + nats_clearLastError(); + + objStore_Destroy(obs); + obs = NULL; + + JS_TEARDOWN; +} + +static bool +_linkPartsAreCorrect(objStoreInfo *linkObj, const char *bucket, const char *name) +{ + if ((strcmp(linkObj->Meta.Opts.Link->Bucket, bucket) != 0) || + (linkObj->ModTime == 0) || nats_IsStringEmpty(linkObj->NUID)) + { + return false; + } + // We know that either `name` is empty, and so make sure same for linkObj, + // otherwise, they need to be equal. + if (nats_IsStringEmpty(linkObj->Meta.Opts.Link->Name) && nats_IsStringEmpty(name)) + return true; + + return (strcmp(linkObj->Meta.Opts.Link->Name, name) == 0); +} + +static bool +_linkIsCorrect(objStoreInfo *origObj, objStoreInfo *linkObj) +{ + if (linkObj->Meta.Opts.Link == NULL) + return false; + + return _linkPartsAreCorrect(linkObj, origObj->Bucket, origObj->Meta.Name); +} + +void +test_ObjectStore_StoreMgtLinks(void) +{ + natsStatus s; + objStore *root = NULL; + objStore *dir = NULL; + objStoreInfo *info = NULL; + objStoreInfo *infoA = NULL; + objStoreInfo *infoLA = NULL; + objStoreInfo *infoB = NULL; + objStoreInfo *infoLB = NULL; + objStoreInfo *infoBucketLink = NULL; + objStoreGet *get = NULL; + char *getLA = NULL; + char *getDbl = NULL; + const char *orgn = NULL; + objStoreConfig cfg; + objStoreOptions o; + + JS_SETUP(2, 10, 0); + +#ifndef NATS_HAS_TLS + nats_hashNoErrorOnNoSSL(true); +#endif + + test("Create root store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "ROOT"; + s = js_CreateObjectStore(&root, js, &cfg); + testCond((s == NATS_OK) && (root != NULL)); + + test("Add some data: "); + s = objStore_PutString(NULL, root, "A", "AAA"); + IFOK(s, objStore_PutString(NULL, root, "B", "BBB")); + IFOK(s, objStore_GetInfo(&infoA, root, "A", NULL)); + testCond((s == NATS_OK) && (infoA != NULL)); + + test("AddLink (bad args): "); + s = objStore_AddLink(&infoLA, NULL, "LA", infoA); + if (s == NATS_INVALID_ARG) + s = objStore_AddLink(&infoLA, root, "LA", NULL); + testCond((s == NATS_INVALID_ARG) && (infoLA == NULL)); + + test("AddLink (bad name): "); + s = objStore_AddLink(&infoLA, root, "", infoA); + if (s == NATS_INVALID_ARG) + s = objStore_AddLink(&infoLA, root, NULL, infoA); + testCond((s == NATS_INVALID_ARG) && (infoLA == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNameIsRequired) != NULL)); + nats_clearLastError(); + + test("Link to individual object: "); + s = objStore_AddLink(&infoLA, root, "LA", infoA); + testCond((s == NATS_OK) && (infoLA != NULL) && _linkIsCorrect(infoA, infoLA)); + + test("Link to a link: "); + s = objStore_AddLink(&info, root, "LALA", infoLA); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNoLinkToLink) != NULL)); + nats_clearLastError(); + + objStoreInfo_Destroy(infoLA); + infoLA = NULL; + + test("Create dir store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "DIR"; + s = js_CreateObjectStore(&dir, js, &cfg); + testCond((s == NATS_OK) && (dir != NULL)); + + test("Add data: "); + s = objStore_PutString(NULL, dir, "DIR/A", "DIR-AAA"); + IFOK(s, objStore_PutString(NULL, dir, "DIR/B", "DIR-BBB")); + IFOK(s, objStore_GetInfo(&infoB, dir, "DIR/B", NULL)); + testCond((s == NATS_OK) && (infoB != NULL)); + + test("Add link dbl: "); + s = objStore_AddLink(&infoLB, root, "DBL", infoB); + testCond((s == NATS_OK) && (infoLB != NULL) && _linkIsCorrect(infoB, infoLB)); + objStoreInfo_Destroy(infoB); + infoB = NULL; + + test("AddBucket (bad args): "); + s = objStore_AddBucketLink(&infoBucketLink, NULL, "dir", dir); + if (s == NATS_INVALID_ARG) + s = objStore_AddBucketLink(&infoBucketLink, root, "dir", NULL); + testCond((s == NATS_INVALID_ARG) && (infoBucketLink == NULL)); + + test("Add whole store as a link: "); + s = objStore_AddBucketLink(&infoBucketLink, root, "dir", dir); + testCond((s == NATS_OK) && (infoBucketLink != NULL)); + + test("Cant get bucket: "); + s = objStore_Get(&get, root, infoBucketLink->Meta.Name, NULL); + testCond((s == NATS_ILLEGAL_STATE) && (get == NULL) && + (strstr(nats_GetLastError(NULL), obsErrCantGetBucket) != NULL)); + nats_clearLastError(); + + test("Link parts correct: "); + testCond(_linkPartsAreCorrect(infoBucketLink, "DIR", NULL)); + objStoreInfo_Destroy(infoBucketLink); + infoBucketLink = NULL; + + test("Get a linked object, same bucket: "); + s = objStore_GetString(&getLA, root, "LA", NULL); + testCond((s == NATS_OK) && !nats_IsStringEmpty(getLA) && (strcmp(getLA, "AAA") == 0)); + free(getLA); + getLA = NULL; + + test("Get a linked object, cross bucket: "); + s = objStore_GetString(&getDbl, root, "DBL", NULL); + testCond((s == NATS_OK) && !nats_IsStringEmpty(getDbl) && (strcmp(getDbl, "DIR-BBB") == 0)); + free(getDbl); + getDbl = NULL; + + test("Get infoB: "); + s = objStore_GetInfo(&infoB, root, "B", NULL); + testCond((s == NATS_OK) && (infoB != NULL)); + + test("Get infoLA: "); + s = objStore_GetInfo(&infoLA, root, "LA", NULL); + testCond((s == NATS_OK) && (infoLA != NULL) && _linkIsCorrect(infoA, infoLA)); + objStoreInfo_Destroy(infoLA); + infoLA = NULL; + objStoreInfo_Destroy(infoA); + infoA = NULL; + + test("AddLink LA to B: "); + s = objStore_AddLink(&infoLA, root, "LA", infoB); + testCond((s == NATS_OK) && (infoLA != NULL) && _linkIsCorrect(infoB, infoLA)); + objStoreInfo_Destroy(infoLA); + infoLA = NULL; + + test("Get info bucket link: "); + s = objStore_GetInfo(&infoBucketLink, root, "dir", NULL); + testCond((s == NATS_OK) && (infoBucketLink != NULL) && + _linkPartsAreCorrect(infoBucketLink, "DIR", NULL)); + objStoreInfo_Destroy(infoBucketLink); + infoBucketLink = NULL; + + test("Change a bucket link: "); + s = objStore_AddBucketLink(&infoBucketLink, root, "dir", root); + testCond((s == NATS_OK) && (infoBucketLink != NULL) && + _linkPartsAreCorrect(infoBucketLink, "ROOT", NULL)); + objStoreInfo_Destroy(infoBucketLink); + infoBucketLink = NULL; + + test("AddLink error (already an object): "); + s = objStore_AddLink(&info, root, "A", infoB); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrObjectAlreadyExists) != NULL)); + nats_clearLastError(); + + test("AddLink error (empty info name): "); + orgn = infoB->Meta.Name; + infoB->Meta.Name = ""; + s = objStore_AddLink(&info, root, "Empty Info Name", infoB); + if (s == NATS_ILLEGAL_STATE) + { + infoB->Meta.Name = NULL; + s = objStore_AddLink(&info, root, "NULL Info Name", infoB); + } + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + infoB->Meta.Name = orgn; + objStoreInfo_Destroy(infoB); + infoB = NULL; + + test("AddLink error (link to link): "); + s = objStore_AddLink(&info, root, "Link To Link", infoLB); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNoLinkToLink) != NULL)); + nats_clearLastError(); + objStoreInfo_Destroy(infoLB); + infoLB = NULL; + + test("Delete object A: "); + s = objStore_Delete(root, "A"); + testCond(s == NATS_OK); + + test("Init options (bad args): "); + s = objStoreOptions_Init(NULL); + testCond(s == NATS_INVALID_ARG); + + test("Get A as deleted: "); + objStoreOptions_Init(&o); + o.ShowDeleted = true; + s = objStore_GetInfo(&infoA, root, "A", &o); + testCond((s == NATS_OK) && (infoA != NULL)); + + test("AddLink error (to deleted): "); + s = objStore_AddLink(&info, root, "To Deleted", infoA); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL) && + (strstr(nats_GetLastError(NULL), obsErrNoLinkToDeleted) != NULL)); + nats_clearLastError(); + objStoreInfo_Destroy(infoA); + infoA = NULL; + + objStore_Destroy(root); + root = NULL; + objStore_Destroy(dir); + dir = NULL; + + JS_TEARDOWN; +} + +void +test_ObjectStore_StoreMgtWatchAndList(void) +{ + natsStatus s; + objStore *obs = NULL; + objStoreWatcher *watch = NULL; + objStoreInfo *info = NULL; + objStoreInfoList *list = NULL; + objStoreConfig cfg; + objStoreOptions so; + objStoreWatchOptions o; + + JS_SETUP(2, 10, 0); + +#ifndef NATS_HAS_TLS + nats_hashNoErrorOnNoSSL(true); +#endif + + test("Create store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "WATCH-TEST"; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Create watcher (bad args): "); + s = objStore_Watch(NULL, obs, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_Watch(&watch, NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (watch == NULL)); + nats_clearLastError(); + + test("Watch: "); + s = objStore_Watch(&watch, obs, NULL); + testCond((s == NATS_OK) && (watch != NULL)); + + test("Get marker: "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info == NULL)); + + test("Add some data: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + IFOK(s, objStore_PutString(NULL, obs, "B", "BBB")); + testCond(s == NATS_OK); + + test("WatchNext (bad args): "); + s = objStoreWatcher_Next(NULL, watch, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreWatcher_Next(&info, NULL, 1000); + if (s == NATS_INVALID_ARG) + s = objStoreWatcher_Next(&info, watch, 0); + if (s == NATS_INVALID_ARG) + s = objStoreWatcher_Next(&info, watch, -10); + testCond((s == NATS_INVALID_ARG) && (info == NULL)); + nats_clearLastError(); + + test("WatchNext (A): "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "A") == 0)); + objStoreInfo_Destroy(info); + info = NULL; + + test("WatchNext (B): "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "B") == 0)); + objStoreInfo_Destroy(info); + info = NULL; + + test("WatchNext (no more): "); + s = objStoreWatcher_Next(&info, watch, 100); + testCond((s == NATS_TIMEOUT) && (info == NULL)); + nats_clearLastError(); + + test("Delete A: "); + s = objStore_Delete(obs, "A"); + testCond(s == NATS_OK); + + test("WatchNext (A): "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "A") == 0) && info->Deleted); + objStoreInfo_Destroy(info); + info = NULL; + + test("WatcherStop (bad args): "); + s = objStoreWatcher_Stop(NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("WatcherStop: "); + s = objStoreWatcher_Stop(watch); + testCond(s == NATS_OK); + + test("WatchNext fails: "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL)); + nats_clearLastError(); + + test("WatchDestroy: "); + objStoreWatcher_Destroy(NULL); + objStoreWatcher_Destroy(watch); + watch = NULL; + testCond(true); + + objStore_Destroy(obs); + obs = NULL; + + // Test with updates only now. + + test("Create store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "WATCH-TEST2"; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Add some data: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + IFOK(s, objStore_PutString(NULL, obs, "B", "BBB")); + testCond(s == NATS_OK); + + test("Init options (bad args): "); + s = objStoreWatchOptions_Init(NULL); + testCond(s == NATS_INVALID_ARG); + nats_clearLastError(); + + test("Create watcher with updates only: "); + objStoreWatchOptions_Init(&o); + o.UpdatesOnly = true; + s = objStore_Watch(&watch, obs, &o); + testCond((s == NATS_OK) && (watch != NULL)); + + // When listening for updates only, we should not receive anything when watcher is started + test("WatchNext (timeout): "); + s = objStoreWatcher_Next(&info, watch, 100); + testCond((s == NATS_TIMEOUT) && (info == NULL)); + nats_clearLastError(); + + test("Delete A: "); + s = objStore_Delete(obs, "A"); + testCond(s == NATS_OK); + + test("WatchNext (A): "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "A") == 0) && info->Deleted); + objStoreInfo_Destroy(info); + info = NULL; + + test("WatchNext (timeout): "); + s = objStoreWatcher_Next(&info, watch, 100); + testCond((s == NATS_TIMEOUT) && (info == NULL)); + nats_clearLastError(); + + test("Add more data: "); + s = objStore_PutString(NULL, obs, "C", "CCC"); + testCond(s == NATS_OK); + + test("WatchNext (C): "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info != NULL) && (strcmp(info->Meta.Name, "C") == 0) && !info->Deleted); + objStoreInfo_Destroy(info); + info = NULL; + + test("WatcherStop: "); + s = objStoreWatcher_Stop(watch); + testCond(s == NATS_OK); + + test("Add more data: "); + s = objStore_PutString(NULL, obs, "D", "DDD"); + testCond(s == NATS_OK); + + test("WatchNext fails: "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_ILLEGAL_STATE) && (info == NULL)); + nats_clearLastError(); + + objStoreWatcher_Destroy(watch); + watch = NULL; + objStore_Destroy(obs); + obs = NULL; + + // Test with ignore delets now. + test("Create store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "WATCH-TEST3"; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("Add A: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + testCond(s == NATS_OK); + + test("Delete A: "); + s = objStore_Delete(obs, "A"); + testCond(s == NATS_OK); + + test("Create watcher with ignore deletes: "); + objStoreWatchOptions_Init(&o); + o.IgnoreDeletes = true; + s = objStore_Watch(&watch, obs, &o); + testCond((s == NATS_OK) && (watch != NULL)); + + test("Get marker: "); + s = objStoreWatcher_Next(&info, watch, 1000); + testCond((s == NATS_OK) && (info == NULL)); + + test("WatchNext (timeout): "); + s = objStoreWatcher_Next(&info, watch, 100); + testCond((s == NATS_TIMEOUT) && (info == NULL)); + nats_clearLastError(); + + objStoreWatcher_Destroy(watch); + watch = NULL; + objStore_Destroy(obs); + obs = NULL; + + // Test list now. + + // Set the initial list capacity size low to make sure we properly grow it. + obsInitialListCap = 2; + + test("Create store: ") + objStoreConfig_Init(&cfg); + cfg.Bucket = "LIST-TEST"; + s = js_CreateObjectStore(&obs, js, &cfg); + testCond((s == NATS_OK) && (obs != NULL)); + + test("List (bad args): "); + s = objStore_List(NULL, obs, NULL); + if (s == NATS_INVALID_ARG) + s = objStore_List(&list, NULL, NULL); + testCond((s == NATS_INVALID_ARG) && (list == NULL)); + nats_clearLastError(); + + test("List (empty): "); + s = objStore_List(&list, obs, NULL); + testCond((s == NATS_NOT_FOUND) && (list == NULL) && (nats_GetLastError(NULL) == NULL)); + + test("Add data: "); + s = objStore_PutString(NULL, obs, "A", "AAA"); + IFOK(s, objStore_PutString(NULL, obs, "B", "BBB")); + IFOK(s, objStore_PutString(NULL, obs, "C", "CCC")); + IFOK(s, objStore_PutString(NULL, obs, "B", "bbb")); + testCond(s == NATS_OK); + + test("Add link: "); + s = objStore_GetInfo(&info, obs, "B", NULL); + IFOK(s, objStore_AddLink(NULL, obs, "b", info)); + testCond((s == NATS_OK) && (info != NULL)); + objStoreInfo_Destroy(info); + info = NULL; + + test("Add and delete data: "); + s = objStore_PutString(NULL, obs, "D", "DDD"); + IFOK(s, objStore_Delete(obs, "D")); + testCond(s == NATS_OK); + + test("List without deletes: "); + s = objStore_List(&list, obs, NULL); + testCond((s == NATS_OK) && (list != NULL) && (list->List != NULL) && (list->Count == 4)); + + test("Check list: "); + testCond((strcmp(list->List[0]->Meta.Name, "A") == 0) && + (strcmp(list->List[1]->Meta.Name, "C") == 0) && + (strcmp(list->List[2]->Meta.Name, "B") == 0) && + (strcmp(list->List[3]->Meta.Name, "b") == 0)); + + test("Destroy list: "); + objStoreInfoList_Destroy(NULL); + objStoreInfoList_Destroy(list); + list = NULL; + testCond(true); + + // Set back to default value and check it works ok. + obsInitialListCap = obsInitialListCapValue; + + test("List with deletes: "); + objStoreOptions_Init(&so); + so.ShowDeleted = true; + s = objStore_List(&list, obs, &so); + testCond((s == NATS_OK) && (list != NULL) && (list->List != NULL) && (list->Count == 5)); + + test("Check list: "); + testCond((strcmp(list->List[0]->Meta.Name, "A") == 0) && + (strcmp(list->List[1]->Meta.Name, "C") == 0) && + (strcmp(list->List[2]->Meta.Name, "B") == 0) && + (strcmp(list->List[3]->Meta.Name, "b") == 0) && + (strcmp(list->List[4]->Meta.Name, "D") == 0)); + objStoreInfoList_Destroy(list); + list = NULL; + + objStore_Destroy(obs); + obs = NULL; + + JS_TEARDOWN; +} + void test_MicroMatchEndpointSubject(void) { // endpoint, actual, match