commit 13354b0eb5e88a03bb9371b8c79dbfef6b0866e0
parent c6f4643b5abbdfdec617054de0ef6e297d0dba50
Author: William Casarin <jb55@jb55.com>
Date: Sun, 9 Apr 2023 22:02:55 -0700
Refactor NIP19 implementation and add tests
Closes: #837
Diffstat:
12 files changed, 738 insertions(+), 346 deletions(-)
diff --git a/damus-c/bech32.c b/damus-c/bech32.c
@@ -91,13 +91,12 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat
return 1;
}
-bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
+bech32_encoding bech32_decode_len(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t input_len) {
uint32_t chk = 1;
size_t i;
- size_t input_len = strlen(input);
size_t hrp_len;
int have_lower = 0, have_upper = 0;
- if (input_len < 8 || input_len > max_input_len) {
+ if (input_len < 8) {
return BECH32_ENCODING_NONE;
}
*data_len = 0;
@@ -154,6 +153,14 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const
}
}
+bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
+ size_t len = strlen(input);
+ if (len > max_input_len) {
+ return BECH32_ENCODING_NONE;
+ }
+ return bech32_decode_len(hrp, data, data_len, input, len);
+}
+
int bech32_convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
diff --git a/damus-c/bech32.h b/damus-c/bech32.h
@@ -118,6 +118,14 @@ bech32_encoding bech32_decode(
size_t max_input_len
);
+bech32_encoding bech32_decode_len(
+ char *hrp,
+ uint8_t *data,
+ size_t *data_len,
+ const char *input,
+ size_t input_len
+);
+
/* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes.
* @outlen is incremented as bytes are added.
* @pad is true if we're to pad, otherwise truncate last byte if necessary
diff --git a/damus-c/block.h b/damus-c/block.h
@@ -0,0 +1,56 @@
+//
+// block.h
+// damus
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+#ifndef block_h
+#define block_h
+
+#include "nostr_bech32.h"
+#include "str_block.h"
+
+#define MAX_BLOCKS 1024
+
+enum block_type {
+ BLOCK_HASHTAG = 1,
+ BLOCK_TEXT = 2,
+ BLOCK_MENTION_INDEX = 3,
+ BLOCK_MENTION_BECH32 = 4,
+ BLOCK_URL = 5,
+ BLOCK_INVOICE = 6,
+};
+
+
+typedef struct invoice_block {
+ struct str_block invstr;
+ union {
+ struct bolt11 *bolt11;
+ };
+} invoice_block_t;
+
+typedef struct mention_bech32_block {
+ struct str_block str;
+ struct nostr_bech32 bech32;
+} mention_bech32_block_t;
+
+typedef struct block {
+ enum block_type type;
+ union {
+ struct str_block str;
+ struct invoice_block invoice;
+ struct mention_bech32_block mention_bech32;
+ int mention_index;
+ } block;
+} block_t;
+
+typedef struct blocks {
+ int num_blocks;
+ struct block *blocks;
+} blocks_t;
+
+void blocks_init(struct blocks *blocks);
+void blocks_free(struct blocks *blocks);
+
+#endif /* block_h */
diff --git a/damus-c/cursor.h b/damus-c/cursor.h
@@ -0,0 +1,150 @@
+//
+// cursor.h
+// damus
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+#ifndef cursor_h
+#define cursor_h
+
+#include <ctype.h>
+#include <string.h>
+
+typedef unsigned char u8;
+
+struct cursor {
+ const u8 *p;
+ const u8 *start;
+ const u8 *end;
+};
+
+static inline int is_whitespace(char c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
+}
+
+static inline int is_boundary(char c) {
+ return !isalnum(c);
+}
+
+static inline int is_invalid_url_ending(char c) {
+ return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
+}
+
+static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
+{
+ c->start = content;
+ c->end = content + len;
+ c->p = content;
+}
+
+static inline int consume_until_boundary(struct cursor *cur) {
+ char c;
+
+ while (cur->p < cur->end) {
+ c = *cur->p;
+
+ if (is_boundary(c))
+ return 1;
+
+ cur->p++;
+ }
+
+ return 1;
+}
+
+static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
+ char c;
+ int consumedAtLeastOne = 0;
+
+ while (cur->p < cur->end) {
+ c = *cur->p;
+
+ if (is_whitespace(c))
+ return consumedAtLeastOne;
+
+ cur->p++;
+ consumedAtLeastOne = 1;
+ }
+
+ return or_end;
+}
+
+static inline int parse_char(struct cursor *cur, char c) {
+ if (cur->p >= cur->end)
+ return 0;
+
+ if (*cur->p == c) {
+ cur->p++;
+ return 1;
+ }
+
+ return 0;
+}
+
+static inline int peek_char(struct cursor *cur, int ind) {
+ if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
+ return -1;
+
+ return *(cur->p + ind);
+}
+
+static int parse_digit(struct cursor *cur, int *digit) {
+ int c;
+ if ((c = peek_char(cur, 0)) == -1)
+ return 0;
+
+ c -= '0';
+
+ if (c >= 0 && c <= 9) {
+ *digit = c;
+ cur->p++;
+ return 1;
+ }
+ return 0;
+}
+
+
+static inline int pull_byte(struct cursor *cur, u8 *byte) {
+ if (cur->p >= cur->end)
+ return 0;
+
+ *byte = *cur->p;
+ cur->p++;
+ return 1;
+}
+
+static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
+ if (cur->p + count > cur->end)
+ return 0;
+
+ *bytes = cur->p;
+ cur->p += count;
+ return 1;
+}
+
+static inline int parse_str(struct cursor *cur, const char *str) {
+ int i;
+ char c, cs;
+ unsigned long len;
+
+ len = strlen(str);
+
+ if (cur->p + len >= cur->end)
+ return 0;
+
+ for (i = 0; i < len; i++) {
+ c = tolower(cur->p[i]);
+ cs = tolower(str[i]);
+
+ if (c != cs)
+ return 0;
+ }
+
+ cur->p += len;
+
+ return 1;
+}
+
+
+#endif /* cursor_h */
diff --git a/damus-c/damus.c b/damus-c/damus.c
@@ -6,130 +6,12 @@
//
#include "damus.h"
+#include "cursor.h"
#include "bolt11.h"
#include "bech32.h"
#include <stdlib.h>
#include <string.h>
-#define TLV_SPECIAL 0
-#define TLV_RELAY 1
-#define TLV_AUTHOR 2
-#define TLV_KIND 3
-
-struct cursor {
- const u8 *p;
- const u8 *start;
- const u8 *end;
-};
-
-static inline int is_whitespace(char c) {
- return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
-}
-
-static inline int is_boundary(char c) {
- return !isalnum(c);
-}
-
-static inline int is_invalid_url_ending(char c) {
- return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
-}
-
-static void make_cursor(struct cursor *c, const u8 *content, size_t len)
-{
- c->start = content;
- c->end = content + len;
- c->p = content;
-}
-
-static int consume_until_boundary(struct cursor *cur) {
- char c;
-
- while (cur->p < cur->end) {
- c = *cur->p;
-
- if (is_boundary(c))
- return 1;
-
- cur->p++;
- }
-
- return 1;
-}
-
-static int consume_until_whitespace(struct cursor *cur, int or_end) {
- char c;
- bool consumedAtLeastOne = false;
-
- while (cur->p < cur->end) {
- c = *cur->p;
-
- if (is_whitespace(c))
- return consumedAtLeastOne;
-
- cur->p++;
- consumedAtLeastOne = true;
- }
-
- return or_end;
-}
-
-static int parse_char(struct cursor *cur, char c) {
- if (cur->p >= cur->end)
- return 0;
-
- if (*cur->p == c) {
- cur->p++;
- return 1;
- }
-
- return 0;
-}
-
-static inline int peek_char(struct cursor *cur, int ind) {
- if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
- return -1;
-
- return *(cur->p + ind);
-}
-
-static int parse_digit(struct cursor *cur, int *digit) {
- int c;
- if ((c = peek_char(cur, 0)) == -1)
- return 0;
-
- c -= '0';
-
- if (c >= 0 && c <= 9) {
- *digit = c;
- cur->p++;
- return 1;
- }
- return 0;
-}
-
-static int parse_str(struct cursor *cur, const char *str) {
- int i;
- char c, cs;
- unsigned long len;
-
- len = strlen(str);
-
- if (cur->p + len >= cur->end)
- return 0;
-
- for (i = 0; i < len; i++) {
- c = tolower(cur->p[i]);
- cs = tolower(str[i]);
-
- if (c != cs)
- return 0;
- }
-
- cur->p += len;
-
- return 1;
-}
-
static int parse_mention_index(struct cursor *cur, struct block *block) {
int d1, d2, d3, ind;
const u8 *start = cur->p;
@@ -278,162 +160,21 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
return 1;
}
-static int parse_mention_bech32(struct cursor *cur, struct block *block) {
- const u8 *start, *start_entity, *end;
- start = cur->p;
+static int parse_mention_bech32(struct cursor *cur, struct block *block) {
+ const u8 *start = cur->p;
+
if (!parse_str(cur, "nostr:"))
return 0;
-
- start_entity = cur->p;
- if (!consume_until_whitespace(cur, 1)) {
- cur->p = start;
- return 0;
- }
-
- end = cur->p;
-
- char str[end - start_entity + 1];
- str[end - start_entity] = 0;
- memcpy(str, start_entity, end - start_entity);
-
- char prefix[end - start_entity];
- u8 data[end - start_entity];
- size_t data_len;
- size_t max_input_len = end - start_entity + 2;
-
- if (bech32_decode(prefix, data, &data_len, str, max_input_len) == BECH32_ENCODING_NONE) {
+
+ if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
cur->p = start;
return 0;
}
-
- struct mention_bech32_block mention = { 0 };
- mention.kind = -1;
- mention.buffer = (u8*)malloc(data_len);
- mention.str.start = (const char*)start;
- mention.str.end = (const char*)end;
-
- size_t len = 0;
- if (!bech32_convert_bits(mention.buffer, &len, 8, data, data_len, 5, 0)) {
- goto fail;
- }
-
- // Parse type
- if (strcmp(prefix, "note") == 0) {
- mention.type = NOSTR_BECH32_NOTE;
- } else if (strcmp(prefix, "npub") == 0) {
- mention.type = NOSTR_BECH32_NPUB;
- } else if (strcmp(prefix, "nprofile") == 0) {
- mention.type = NOSTR_BECH32_NPROFILE;
- } else if (strcmp(prefix, "nevent") == 0) {
- mention.type = NOSTR_BECH32_NEVENT;
- } else if (strcmp(prefix, "nrelay") == 0) {
- mention.type = NOSTR_BECH32_NRELAY;
- } else if (strcmp(prefix, "naddr") == 0) {
- mention.type = NOSTR_BECH32_NADDR;
- } else {
- goto fail;
- }
-
- // Parse notes and npubs (non-TLV)
- if (mention.type == NOSTR_BECH32_NOTE || mention.type == NOSTR_BECH32_NPUB) {
- if (len != 32) goto fail;
- if (mention.type == NOSTR_BECH32_NOTE) {
- mention.event_id = mention.buffer;
- } else {
- mention.pubkey = mention.buffer;
- }
- goto ok;
- }
-
- // Parse TLV entities
- const int MAX_VALUES = 16;
- int values_count = 0;
- u8 Ts[MAX_VALUES];
- u8 Ls[MAX_VALUES];
- u8* Vs[MAX_VALUES];
- for (int i = 0; i < len - 1;) {
- if (values_count == MAX_VALUES) goto fail;
-
- Ts[values_count] = mention.buffer[i++];
- Ls[values_count] = mention.buffer[i++];
- if (Ls[values_count] > len - i) goto fail;
-
- Vs[values_count] = &mention.buffer[i];
- i += Ls[values_count];
- ++values_count;
- }
-
- // Decode and validate all TLV-type entities
- if (mention.type == NOSTR_BECH32_NPROFILE) {
- for (int i = 0; i < values_count; ++i) {
- if (Ts[i] == TLV_SPECIAL) {
- if (Ls[i] != 32 || mention.pubkey) goto fail;
- mention.pubkey = Vs[i];
- } else if (Ts[i] == TLV_RELAY) {
- if (mention.relays_count == MAX_RELAYS) goto fail;
- Vs[i][Ls[i]] = 0;
- mention.relays[mention.relays_count++] = (char*)Vs[i];
- } else {
- goto fail;
- }
- }
- if (!mention.pubkey) goto fail;
-
- } else if (mention.type == NOSTR_BECH32_NEVENT) {
- for (int i = 0; i < values_count; ++i) {
- if (Ts[i] == TLV_SPECIAL) {
- if (Ls[i] != 32 || mention.event_id) goto fail;
- mention.event_id = Vs[i];
- } else if (Ts[i] == TLV_RELAY) {
- if (mention.relays_count == MAX_RELAYS) goto fail;
- Vs[i][Ls[i]] = 0;
- mention.relays[mention.relays_count++] = (char*)Vs[i];
- } else if (Ts[i] == TLV_AUTHOR) {
- if (Ls[i] != 32 || mention.pubkey) goto fail;
- mention.pubkey = Vs[i];
- } else {
- goto fail;
- }
- }
- if (!mention.event_id) goto fail;
-
- } else if (mention.type == NOSTR_BECH32_NRELAY) {
- if (values_count != 1 || Ts[0] != TLV_SPECIAL) goto fail;
- Vs[0][Ls[0]] = 0;
- mention.relays[mention.relays_count++] = (char*)Vs[0];
-
- } else { // entity.type == NOSTR_BECH32_NADDR
- for (int i = 0; i < values_count; ++i) {
- if (Ts[i] == TLV_SPECIAL) {
- Vs[i][Ls[i]] = 0;
- mention.identifier = (char*)Vs[i];
- } else if (Ts[i] == TLV_RELAY) {
- if (mention.relays_count == MAX_RELAYS) goto fail;
- Vs[i][Ls[i]] = 0;
- mention.relays[mention.relays_count++] = (char*)Vs[i];
- } else if (Ts[i] == TLV_AUTHOR) {
- if (Ls[i] != 32 || mention.pubkey) goto fail;
- mention.pubkey = Vs[i];
- } else if (Ts[i] == TLV_KIND) {
- if (Ls[i] != sizeof(int) || mention.kind != -1) goto fail;
- mention.kind = *(int*)Vs[i];
- } else {
- goto fail;
- }
- }
- if (!mention.identifier || mention.kind == -1 || !mention.pubkey) goto fail;
- }
-
-ok:
+
block->type = BLOCK_MENTION_BECH32;
- block->block.mention_bech32 = mention;
- return 1;
-fail:
- free(mention.buffer);
- cur->p = start;
- return 0;
+ return 1;
}
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
@@ -504,11 +245,11 @@ void blocks_free(struct blocks *blocks) {
if (!blocks->blocks) {
return;
}
-
+
for (int i = 0; i < blocks->num_blocks; ++i) {
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
- free(blocks->blocks[i].block.mention_bech32.buffer);
- blocks->blocks[i].block.mention_bech32.buffer = NULL;
+ free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
+ blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
}
}
diff --git a/damus-c/damus.h b/damus-c/damus.h
@@ -9,72 +9,10 @@
#define damus_h
#include <stdio.h>
+#include "nostr_bech32.h"
+#include "block.h"
typedef unsigned char u8;
-#define MAX_BLOCKS 1024
-#define MAX_RELAYS 10
-
-enum block_type {
- BLOCK_HASHTAG = 1,
- BLOCK_TEXT = 2,
- BLOCK_MENTION_INDEX = 3,
- BLOCK_MENTION_BECH32 = 4,
- BLOCK_URL = 5,
- BLOCK_INVOICE = 6,
-};
-
-enum nostr_bech32_type {
- NOSTR_BECH32_NOTE = 1,
- NOSTR_BECH32_NPUB = 2,
- NOSTR_BECH32_NPROFILE = 3,
- NOSTR_BECH32_NEVENT = 4,
- NOSTR_BECH32_NRELAY = 5,
- NOSTR_BECH32_NADDR = 6,
-};
-
-typedef struct str_block {
- const char *start;
- const char *end;
-} str_block_t;
-
-typedef struct invoice_block {
- struct str_block invstr;
- union {
- struct bolt11 *bolt11;
- };
-} invoice_block_t;
-
-typedef struct mention_bech32_block {
- struct str_block str;
- enum nostr_bech32_type type;
-
- u8 *event_id;
- u8 *pubkey;
- char *identifier;
- char *relays[MAX_RELAYS];
- int relays_count;
- int kind;
-
- u8* buffer;
-} mention_bech32_block_t;
-
-typedef struct block {
- enum block_type type;
- union {
- struct str_block str;
- struct invoice_block invoice;
- struct mention_bech32_block mention_bech32;
- int mention_index;
- } block;
-} block_t;
-
-typedef struct blocks {
- int num_blocks;
- struct block *blocks;
-} blocks_t;
-
-void blocks_init(struct blocks *blocks);
-void blocks_free(struct blocks *blocks);
int damus_parse_content(struct blocks *blocks, const char *content);
#endif /* damus_h */
diff --git a/damus-c/nostr_bech32.c b/damus-c/nostr_bech32.c
@@ -0,0 +1,295 @@
+//
+// nostr_bech32.c
+// damus
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+#include "nostr_bech32.h"
+#include <stdlib.h>
+#include "cursor.h"
+#include "bech32.h"
+
+#define MAX_TLVS 16
+
+#define TLV_SPECIAL 0
+#define TLV_RELAY 1
+#define TLV_AUTHOR 2
+#define TLV_KIND 3
+#define TLV_KNOWN_TLVS 4
+
+struct nostr_tlv {
+ u8 type;
+ u8 len;
+ const u8 *value;
+};
+
+struct nostr_tlvs {
+ struct nostr_tlv tlvs[MAX_TLVS];
+ int num_tlvs;
+};
+
+static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
+ // get the tlv tag
+ if (!pull_byte(cur, &tlv->type))
+ return 0;
+
+ // unknown, fail!
+ if (tlv->type >= TLV_KNOWN_TLVS)
+ return 0;
+
+ // get the length
+ if (!pull_byte(cur, &tlv->len))
+ return 0;
+
+ // is the reported length greater then our buffer? if so fail
+ if (cur->p + tlv->len > cur->end)
+ return 0;
+
+ tlv->value = cur->p;
+ cur->p += tlv->len;
+
+ return 1;
+}
+
+static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
+ int i;
+ tlvs->num_tlvs = 0;
+
+ for (i = 0; i < MAX_TLVS; i++) {
+ if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
+ tlvs->num_tlvs++;
+ } else {
+ break;
+ }
+ }
+
+ if (tlvs->num_tlvs == 0)
+ return 0;
+
+ return 1;
+}
+
+static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
+ *tlv = NULL;
+
+ for (int i = 0; i < tlvs->num_tlvs; i++) {
+ if (tlvs->tlvs[i].type == type) {
+ *tlv = &tlvs->tlvs[i];
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
+ // Parse type
+ if (strcmp(prefix, "note") == 0) {
+ *type = NOSTR_BECH32_NOTE;
+ return 1;
+ } else if (strcmp(prefix, "npub") == 0) {
+ *type = NOSTR_BECH32_NPUB;
+ return 1;
+ } else if (strcmp(prefix, "nprofile") == 0) {
+ *type = NOSTR_BECH32_NPROFILE;
+ return 1;
+ } else if (strcmp(prefix, "nevent") == 0) {
+ *type = NOSTR_BECH32_NEVENT;
+ return 1;
+ } else if (strcmp(prefix, "nrelay") == 0) {
+ *type = NOSTR_BECH32_NRELAY;
+ return 1;
+ } else if (strcmp(prefix, "naddr") == 0) {
+ *type = NOSTR_BECH32_NADDR;
+ return 1;
+ }
+
+ return 0;
+}
+
+static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
+ return pull_bytes(cur, 32, ¬e->event_id);
+}
+
+static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
+ return pull_bytes(cur, 32, &npub->pubkey);
+}
+
+static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
+ struct nostr_tlv *tlv;
+ struct str_block *str;
+
+ relays->num_relays = 0;
+
+ for (int i = 0; i < tlvs->num_tlvs; i++) {
+ tlv = &tlvs->tlvs[i];
+ if (tlv->type != TLV_RELAY)
+ continue;
+
+ if (relays->num_relays + 1 > MAX_RELAYS)
+ break;
+
+ str = &relays->relays[relays->num_relays++];
+ str->start = (const char*)tlv->value;
+ str->end = (const char*)(tlv->value + tlv->len);
+ }
+
+ return relays->num_relays > 0;
+}
+
+static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
+ struct nostr_tlvs tlvs;
+ struct nostr_tlv *tlv;
+
+ if (!parse_nostr_tlvs(cur, &tlvs))
+ return 0;
+
+ if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
+ return 0;
+
+ if (tlv->len != 32)
+ return 0;
+
+ nevent->event_id = tlv->value;
+
+ if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
+ nevent->pubkey = tlv->value;
+ } else {
+ nevent->pubkey = NULL;
+ }
+
+ return tlvs_to_relays(&tlvs, &nevent->relays);
+}
+
+static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
+ struct nostr_tlvs tlvs;
+ struct nostr_tlv *tlv;
+
+ if (!parse_nostr_tlvs(cur, &tlvs))
+ return 0;
+
+ if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
+ return 0;
+
+ naddr->identifier.start = (const char*)tlv->value;
+ naddr->identifier.end = (const char*)tlv->value + tlv->len;
+
+ if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
+ return 0;
+
+ naddr->pubkey = tlv->value;
+
+ return tlvs_to_relays(&tlvs, &naddr->relays);
+}
+
+static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
+ struct nostr_tlvs tlvs;
+ struct nostr_tlv *tlv;
+
+ if (!parse_nostr_tlvs(cur, &tlvs))
+ return 0;
+
+ if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
+ return 0;
+
+ if (tlv->len != 32)
+ return 0;
+
+ nprofile->pubkey = tlv->value;
+
+ return tlvs_to_relays(&tlvs, &nprofile->relays);
+}
+
+static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
+ struct nostr_tlvs tlvs;
+ struct nostr_tlv *tlv;
+
+ if (!parse_nostr_tlvs(cur, &tlvs))
+ return 0;
+
+ if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
+ return 0;
+
+ nrelay->relay.start = (const char*)tlv->value;
+ nrelay->relay.end = (const char*)tlv->value + tlv->len;
+
+ return 1;
+}
+
+int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
+ const u8 *start, *end;
+
+ start = cur->p;
+
+ if (!consume_until_whitespace(cur, 1)) {
+ cur->p = start;
+ return 0;
+ }
+
+ end = cur->p;
+
+ size_t data_len;
+ size_t input_len = end - start;
+ if (input_len < 10 || input_len > 10000) {
+ return 0;
+ }
+
+ obj->buffer = malloc(input_len * 2);
+ if (!obj->buffer)
+ return 0;
+
+ u8 data[input_len];
+ char prefix[input_len];
+
+ if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
+ cur->p = start;
+ return 0;
+ }
+
+ obj->buflen = 0;
+ if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
+ goto fail;
+ }
+
+ if (!parse_nostr_bech32_type(prefix, &obj->type)) {
+ goto fail;
+ }
+
+ struct cursor bcur;
+ make_cursor(&bcur, obj->buffer, obj->buflen);
+
+ switch (obj->type) {
+ case NOSTR_BECH32_NOTE:
+ if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
+ goto fail;
+ break;
+ case NOSTR_BECH32_NPUB:
+ if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
+ goto fail;
+ break;
+ case NOSTR_BECH32_NEVENT:
+ if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
+ goto fail;
+ break;
+ case NOSTR_BECH32_NADDR:
+ if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
+ goto fail;
+ break;
+ case NOSTR_BECH32_NPROFILE:
+ if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
+ goto fail;
+ break;
+ case NOSTR_BECH32_NRELAY:
+ if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
+ goto fail;
+ break;
+ }
+
+ return 1;
+
+fail:
+ free(obj->buffer);
+ cur->p = start;
+ return 0;
+}
diff --git a/damus-c/nostr_bech32.h b/damus-c/nostr_bech32.h
@@ -0,0 +1,78 @@
+//
+// nostr_bech32.h
+// damus
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+#ifndef nostr_bech32_h
+#define nostr_bech32_h
+
+#include <stdio.h>
+#include "str_block.h"
+#include "cursor.h"
+typedef unsigned char u8;
+#define MAX_RELAYS 10
+
+struct relays {
+ struct str_block relays[MAX_RELAYS];
+ int num_relays;
+};
+
+enum nostr_bech32_type {
+ NOSTR_BECH32_NOTE = 1,
+ NOSTR_BECH32_NPUB = 2,
+ NOSTR_BECH32_NPROFILE = 3,
+ NOSTR_BECH32_NEVENT = 4,
+ NOSTR_BECH32_NRELAY = 5,
+ NOSTR_BECH32_NADDR = 6,
+};
+
+struct bech32_note {
+ const u8 *event_id;
+};
+
+struct bech32_npub {
+ const u8 *pubkey;
+};
+
+struct bech32_nevent {
+ struct relays relays;
+ const u8 *event_id;
+ const u8 *pubkey; // optional
+};
+
+struct bech32_nprofile {
+ struct relays relays;
+ const u8 *pubkey;
+};
+
+struct bech32_naddr {
+ struct relays relays;
+ struct str_block identifier;
+ const u8 *pubkey;
+};
+
+struct bech32_nrelay {
+ struct str_block relay;
+};
+
+typedef struct nostr_bech32 {
+ enum nostr_bech32_type type;
+ u8 *buffer; // holds strings and tlv stuff
+ size_t buflen;
+
+ union {
+ struct bech32_note note;
+ struct bech32_npub npub;
+ struct bech32_nevent nevent;
+ struct bech32_nprofile nprofile;
+ struct bech32_naddr naddr;
+ struct bech32_nrelay nrelay;
+ } data;
+} nostr_bech32_t;
+
+
+int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
+
+#endif /* nostr_bech32_h */
diff --git a/damus-c/str_block.h b/damus-c/str_block.h
@@ -0,0 +1,16 @@
+//
+// str_block.h
+// damus
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+#ifndef str_block_h
+#define str_block_h
+
+typedef struct str_block {
+ const char *start;
+ const char *end;
+} str_block_t;
+
+#endif /* str_block_h */
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -139,6 +139,8 @@
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C929DF80350036AF10 /* TruncatedText.swift */; };
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
+ 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
+ 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
@@ -541,6 +543,12 @@
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
+ 4C8D00CD29E38B950036AF10 /* nostr_bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostr_bech32.h; sourceTree = "<group>"; };
+ 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostr_bech32.c; sourceTree = "<group>"; };
+ 4C8D00D029E38E4C0036AF10 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
+ 4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
+ 4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
+ 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
@@ -777,6 +785,11 @@
4C3EA67428FF7A5A00C48A62 /* take.c */,
4C3EA67628FF7A9800C48A62 /* talstr.c */,
4C3EA67828FF7ABF00C48A62 /* list.c */,
+ 4C8D00CD29E38B950036AF10 /* nostr_bech32.h */,
+ 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */,
+ 4C8D00D029E38E4C0036AF10 /* cursor.h */,
+ 4C8D00D129E397AD0036AF10 /* block.h */,
+ 4C8D00D229E3C19F0036AF10 /* str_block.h */,
);
path = "damus-c";
sourceTree = "<group>";
@@ -1186,6 +1199,7 @@
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
+ 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1639,6 +1653,7 @@
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
+ 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
@@ -1710,6 +1725,7 @@
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
+ 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift
@@ -21,7 +21,7 @@ enum MentionType {
}
}
-struct Mention {
+struct Mention: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
@@ -58,7 +58,24 @@ struct LightningInvoice<T> {
}
}
-enum Block {
+enum Block: Equatable {
+ static func == (lhs: Block, rhs: Block) -> Bool {
+ switch (lhs, rhs) {
+ case (.text(let a), .text(let b)):
+ return a == b
+ case (.mention(let a), .mention(let b)):
+ return a == b
+ case (.hashtag(let a), .hashtag(let b)):
+ return a == b
+ case (.url(let a), .url(let b)):
+ return a == b
+ case (.invoice(let a), .invoice(let b)):
+ return a.string == b.string
+ case (_, _):
+ return false
+ }
+ }
+
case text(String)
case mention(Mention)
case hashtag(String)
@@ -317,20 +334,30 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
{
- let relay_id = b.relays_count > 0 ? String(cString: b.relays.0!) : nil
-
- switch b.type {
+ switch b.bech32.type {
case NOSTR_BECH32_NOTE:
- fallthrough
+ let note = b.bech32.data.note;
+ let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
+ let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
+ return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
+
case NOSTR_BECH32_NEVENT:
- let event_id = hex_encode(Data(bytes: b.event_id, count: 32))
+ let nevent = b.bech32.data.nevent;
+ let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
+ let relay_id = strblock_to_string(nevent.relays.relays.0)
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NPUB:
- fallthrough
+ let npub = b.bech32.data.npub
+ let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
+ let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
+ return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
+
case NOSTR_BECH32_NPROFILE:
- let pubkey = hex_encode(Data(bytes: b.pubkey, count: 32))
+ let nprofile = b.bech32.data.nprofile
+ let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
+ let relay_id = strblock_to_string(nprofile.relays.relays.0)
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
diff --git a/damusTests/NIP19Tests.swift b/damusTests/NIP19Tests.swift
@@ -0,0 +1,60 @@
+//
+// NIP19Tests.swift
+// damusTests
+//
+// Created by William Casarin on 2023-04-09.
+//
+
+import XCTest
+@testable import damus
+
+final class NIP19Tests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func test_parse_nprofile() throws {
+ let res = parse_mentions(content: "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", tags: [])
+ XCTAssertEqual(res.count, 1)
+ let expected_ref = ReferencedId(ref_id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", relay_id: "wss://r.x.com", key: "p")
+ let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref)
+ XCTAssertEqual(res[0], .mention(expected_mention))
+ }
+
+ func test_parse_npub() throws {
+ let res = parse_mentions(content: "nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg ", tags: [])
+ XCTAssertEqual(res.count, 2)
+ let expected_ref = ReferencedId(ref_id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", relay_id: nil, key: "p")
+ let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref)
+ XCTAssertEqual(res[0], .mention(expected_mention))
+ }
+
+ func test_parse_note() throws {
+ let res = parse_mentions(content: " nostr:note1s4p70596lv50x0zftuses32t6ck8x6wgd4edwacyetfxwns2jtysux7vep", tags: [])
+ XCTAssertEqual(res.count, 2)
+ let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "e")
+ let expected_mention = Mention(index: nil, type: .event, ref: expected_ref)
+ XCTAssertEqual(res[1], .mention(expected_mention))
+ }
+
+ func test_parse_nevent() throws {
+ let res = parse_mentions(content: " nostr:nprofile", tags: [])
+ XCTAssertEqual(res.count, 2)
+ let expected_ref = ReferencedId(ref_id: "8543e7d0bafb28f33c495f2198454bd62c7369c86d72d77704cad2674e0a92c9", relay_id: nil, key: "p")
+ let expected_mention = Mention(index: nil, type: .pubkey, ref: expected_ref)
+ XCTAssertEqual(res[1], .mention(expected_mention))
+ }
+
+ func testPerformanceExample() throws {
+ // This is an example of a performance test case.
+ self.measure {
+ // Put the code you want to measure the time of here.
+ }
+ }
+
+}