nostrdb

an unfairly fast embedded nostr database backed by lmdb
git clone git://jb55.com/nostrdb
Log | Files | Refs | README | LICENSE

commit 32b0e572e418b81af515224cc7eb3b1e8598d906
parent 5e236cdcbb7ab0a9cb8ee9206ba4bdbc69bae3a1
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 20 Jul 2023 00:55:43 -0700

initial json decoding

Diffstat:
MMakefile | 5++---
Ahex.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ajsmn.h | 471+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnostrdb.c | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mnostrdb.h | 29++++++++++++++++-------------
Mtest.c | 31++++++++++++++++++++++++++-----
6 files changed, 663 insertions(+), 27 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,5 +1,4 @@ - -CFLAGS = -Wall -Werror -O3 +CFLAGS = -Wall -Wno-unused-function -Werror -O3 -DJSMN_PARENT_LINKS check: test ./test @@ -11,7 +10,7 @@ tags: ctags *.c *.h test: test.c nostrdb.c nostrdb.h - $(CC) test.c nostrdb.c -o $@ + $(CC) $(CFLAGS) test.c nostrdb.c -o $@ %.o: %.c $(CC) $(CFLAGS) diff --git a/hex.h b/hex.h @@ -0,0 +1,67 @@ + +#ifndef HEX_H +#define HEX_H + +#include <stdlib.h> + +static int char_to_hex(unsigned char *val, char c) +{ + if (c >= '0' && c <= '9') { + *val = c - '0'; + return 1; + } + if (c >= 'a' && c <= 'f') { + *val = c - 'a' + 10; + return 1; + } + if (c >= 'A' && c <= 'F') { + *val = c - 'A' + 10; + return 1; + } + return 0; +} + +static int hex_decode(const char *str, size_t slen, void *buf, size_t bufsize) +{ + unsigned char v1, v2; + unsigned char *p = buf; + + while (slen > 1) { + if (!char_to_hex(&v1, str[0]) || !char_to_hex(&v2, str[1])) + return 0; + if (!bufsize) + return 0; + *(p++) = (v1 << 4) | v2; + str += 2; + slen -= 2; + bufsize--; + } + return slen == 0 && bufsize == 0; +} + + +static char hexchar(unsigned int val) +{ + if (val < 10) + return '0' + val; + if (val < 16) + return 'a' + val - 10; + abort(); +} + +static int hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize) +{ + size_t i; + + for (i = 0; i < bufsize; i++) { + unsigned int c = ((const unsigned char *)buf)[i]; + *(dest++) = hexchar(c >> 4); + *(dest++) = hexchar(c & 0xF); + } + *dest = '\0'; + + return 1; +} + + +#endif diff --git a/jsmn.h b/jsmn.h @@ -0,0 +1,471 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct jsmntok { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if (token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ diff --git a/nostrdb.c b/nostrdb.c @@ -1,5 +1,7 @@ #include "nostrdb.h" +#include "jsmn.h" +#include "hex.h" #include <stdlib.h> int ndb_builder_new(struct ndb_builder *builder, int *bufsize) { @@ -10,7 +12,7 @@ int ndb_builder_new(struct ndb_builder *builder, int *bufsize) { if (bufsize) builder->size = *bufsize; - int str_indices_size = sizeof(uint32_t) * (2<<14); + int str_indices_size = builder->size / 32; unsigned char *bytes = malloc(builder->size + str_indices_size); if (!bytes) return 0; @@ -52,8 +54,7 @@ struct ndb_note *ndb_builder_note(struct ndb_builder *builder) { return builder->note; } -int ndb_builder_make_string(struct ndb_builder *builder, const char *str, union packed_str *pstr) { - int len = strlen(str); +int ndb_builder_make_string(struct ndb_builder *builder, const char *str, int len, union packed_str *pstr) { uint32_t loc; if (len == 0) { @@ -95,8 +96,82 @@ int ndb_builder_make_string(struct ndb_builder *builder, const char *str, union return 1; } -int ndb_builder_set_content(struct ndb_builder *builder, const char *content) { - return ndb_builder_make_string(builder, content, &builder->note->content); +int ndb_builder_set_content(struct ndb_builder *builder, const char *content, int len) { + return ndb_builder_make_string(builder, content, len, &builder->note->content); +} + + +static inline int jsoneq(const char *json, jsmntok_t *tok, int tok_len, const char *s) { + if (tok->type == JSMN_STRING && (int)strlen(s) == tok_len && + memcmp(json + tok->start, s, tok_len) == 0) { + return 1; + } + return 0; +} + +int ndb_note_from_json(const char *json, int len, struct ndb_note **note) { + int i, r; + struct ndb_builder builder; + jsmn_parser p; + jsmntok_t toks[4096], *tok = NULL; + unsigned char buf[64]; + jsmn_init(&p); + int tok_len; + const char *start; + + ndb_builder_new(&builder, &len); + + r = jsmn_parse(&p, json, len, toks, sizeof(toks)/sizeof(toks[0])); + + if (r < 0) return 0; + if (r < 1 || toks[0].type != JSMN_OBJECT) return 0; + + for (i = 1; i < r; i++) { + tok = &toks[i]; + tok_len = tok->end - tok->start; + start = json + tok->start; + if (tok_len == 0 || i + 1 >= r) + continue; + + if (start[0] == 'p' && jsoneq(json, tok, tok_len, "pubkey")) { + // pubkey + tok = &toks[i+1]; tok_len = tok->end - tok->start; + hex_decode(json + tok->start, tok_len, buf, sizeof(buf)); + ndb_builder_set_pubkey(&builder, buf); + } else if (tok_len == 2 && start[0] == 'i' && start[1] == 'd') { + // id + tok = &toks[i+1]; tok_len = tok->end - tok->start; + hex_decode(json + tok->start, tok_len, buf, sizeof(buf)); + // TODO: validate id + ndb_builder_set_id(&builder, buf); + } else if (tok_len == 3 && start[0] == 's' && start[1] == 'i' && start[2] == 'g') { + // sig + tok = &toks[i+1]; tok_len = tok->end - tok->start; + hex_decode(json + tok->start, tok_len, buf, sizeof(buf)); + ndb_builder_set_signature(&builder, buf); + } else if (start[0] == 'k' && jsoneq(json, tok, tok_len, "kind")) { + // kind + tok = &toks[i+1]; tok_len = tok->end - tok->start; + printf("json_kind %.*s\n", tok_len, json + tok->start); + } else if (start[0] == 'c') { + if (jsoneq(json, tok, tok_len, "created_at")) { + // created_at + tok = &toks[i+1]; tok_len = tok->end - tok->start; + printf("json_created_at %.*s\n", tok_len, json + tok->start); + } else if (jsoneq(json, tok, tok_len, "content")) { + // content + tok = &toks[i+1]; tok_len = tok->end - tok->start; + if (!ndb_builder_set_content(&builder, json + tok->start, tok_len)) + printf("json_content %.*s\n", tok_len, json + tok->start); + } + } else if (start[0] == 't' && jsoneq(json, tok, tok_len, "tags")) { + // tags + tok = &toks[i+1]; tok_len = tok->end - tok->start; + printf("json_tags %.*s\n", tok_len, json + tok->start); + } + } + + return ndb_builder_finalize(&builder, note); } void ndb_builder_set_pubkey(struct ndb_builder *builder, unsigned char *pubkey) { @@ -133,7 +208,7 @@ int ndb_builder_add_tag(struct ndb_builder *builder, const char **strs, uint16_t for (i = 0; i < num_strs; i++) { str = strs[i]; - if (!ndb_builder_make_string(builder, str, &pstr)) + if (!ndb_builder_make_string(builder, str, strlen(str), &pstr)) return 0; if (!cursor_push_u32(&builder->note_cur, pstr.offset)) return 0; diff --git a/nostrdb.h b/nostrdb.h @@ -66,15 +66,28 @@ struct ndb_iterator { int index; }; +int ndb_note_from_json(const char *json, int len, struct ndb_note **); int ndb_builder_new(struct ndb_builder *builder, int *bufsize); int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note); -int ndb_builder_set_content(struct ndb_builder *builder, const char *content); +int ndb_builder_set_content(struct ndb_builder *builder, const char *content, int len); void ndb_builder_set_signature(struct ndb_builder *builder, unsigned char *signature); void ndb_builder_set_pubkey(struct ndb_builder *builder, unsigned char *pubkey); void ndb_builder_set_id(struct ndb_builder *builder, unsigned char *id); void ndb_builder_set_kind(struct ndb_builder *builder, uint32_t kind); int ndb_builder_add_tag(struct ndb_builder *builder, const char **strs, uint16_t num_strs); +static inline int ndb_str_is_packed(union packed_str str) { + return (str.offset >> 31) & 0x1; +} + +static inline const char * +ndb_note_string(struct ndb_note *note, union packed_str *str) { + if (ndb_str_is_packed(*str)) + return str->packed.str; + + return ((const char *)note) + note->strings + str->offset; +} + static inline unsigned char *ndb_note_id(struct ndb_note *note) { return note->id; } @@ -91,8 +104,8 @@ static inline uint32_t ndb_note_created_at(struct ndb_note *note) { return note->created_at; } -static inline int ndb_str_is_packed(union packed_str str) { - return (str.offset >> 31) & 0x1; +static inline const char *ndb_note_content(struct ndb_note *note) { + return ndb_note_string(note, &note->content); } static inline struct ndb_note *ndb_note_from_bytes(unsigned char *bytes) { @@ -129,14 +142,6 @@ ndb_chars_to_packed_str(char c1, char c2) { } static inline const char * -ndb_note_string(struct ndb_note *note, union packed_str *str) { - if (ndb_str_is_packed(*str)) - return str->packed.str; - - return ((const char *)note) + note->strings + str->offset; -} - -static inline const char * ndb_note_tag_index(struct ndb_note *note, struct ndb_tag *tag, int index) { if (index >= tag->count) { return 0; @@ -147,8 +152,6 @@ ndb_note_tag_index(struct ndb_note *note, struct ndb_tag *tag, int index) { static inline int ndb_tags_iterate_start(struct ndb_note *note, struct ndb_iterator *iter) { - uint16_t count = note->tags.count; - iter->note = note; iter->tag = note->tags.tag; iter->index = 0; diff --git a/test.c b/test.c @@ -1,5 +1,6 @@ #include "nostrdb.h" +#include "hex.h" #include <stdio.h> #include <assert.h> @@ -9,7 +10,7 @@ static void test_basic_event() { struct ndb_builder builder, *b = &builder; struct ndb_note *note; - int len, ok; + int ok; unsigned char id[32]; memset(id, 1, 32); @@ -30,14 +31,15 @@ static void test_basic_event() { memset(note->padding, 3, sizeof(note->padding)); - ndb_builder_set_content(b, "hello, world!"); + const char *content = "hello, world!"; + ndb_builder_set_content(b, content, strlen(content)); ndb_builder_set_id(b, id); ndb_builder_set_pubkey(b, pubkey); ndb_builder_set_signature(b, sig); ndb_builder_add_tag(b, tag, ARRAY_SIZE(tag)); ndb_builder_add_tag(b, word_tag, ARRAY_SIZE(word_tag)); - len = ndb_builder_finalize(b, &note); + ndb_builder_finalize(b, &note); assert(note->tags.count == 2); @@ -69,10 +71,10 @@ static void test_empty_tags() { struct ndb_builder builder, *b = &builder; struct ndb_iterator iter, *it = &iter; struct ndb_note *note; - int ok, len; + int ok; ndb_builder_new(b, 0); - len = ndb_builder_finalize(b, &note); + ndb_builder_finalize(b, &note); assert(note->tags.count == 0); @@ -80,7 +82,26 @@ static void test_empty_tags() { assert(!ok); } + +static void test_parse_json() { + char hex_id[65] = {0}; + struct ndb_note *note; +#define HEX_ID "5004a081e397c6da9dc2f2d6b3134006a9d0e8c1b46689d9fe150bb2f21a204d" + static const char *json = + "{\"id\": \"" HEX_ID "\",\"pubkey\": \"b169f596968917a1abeb4234d3cf3aa9baee2112e58998d17c6db416ad33fe40\",\"created_at\": 1689836342,\"kind\": 1,\"tags\": [],\"content\": \"共通語\",\"sig\": \"e4d528651311d567f461d7be916c37cbf2b4d530e672f29f15f353291ed6df60c665928e67d2f18861c5ca88\"}"; + + ndb_note_from_json(json, strlen(json), &note); + + const char *content = ndb_note_content(note); + unsigned char *id = ndb_note_id(note); + hex_encode(id, 32, hex_id, sizeof(hex_id)); + + assert(!strcmp(content, "共通語")); + assert(!strcmp(HEX_ID, hex_id)); +} + int main(int argc, const char *argv[]) { test_basic_event(); test_empty_tags(); + test_parse_json(); }