damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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:
Mdamus-c/bech32.c | 13++++++++++---
Mdamus-c/bech32.h | 8++++++++
Adamus-c/block.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/cursor.h | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus-c/damus.c | 281++++---------------------------------------------------------------------------
Mdamus-c/damus.h | 66++----------------------------------------------------------------
Adamus-c/nostr_bech32.c | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/nostr_bech32.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus-c/str_block.h | 16++++++++++++++++
Mdamus.xcodeproj/project.pbxproj | 16++++++++++++++++
Mdamus/Models/Mentions.swift | 45++++++++++++++++++++++++++++++++++++---------
AdamusTests/NIP19Tests.swift | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, &note->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. + } + } + +}