Fix broken DM rendering
Currently NostrDB does not seem to handle encryption/decryption of DMs. Since NostrDB now controls the block parsing process and fetches note contents directly from the database, we have to add a specific condition that injects decrypted content directly to the ndb content parser. This is done in conjunction with some minor refactoring to `NdbBlocks` and associated structs, as in C those are separated between the content string and the offsets for each block, but in Swift this is more ergonomically represented as a standalone/self-containing object. No changelog entry is added because the previously broken version was never released to the public, and therefore this fix produces no user-facing changes compared to the last released version. Changelog-None Closes: https://github.com/damus-io/damus/issues/3106 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -227,16 +227,16 @@ class Ndb {
|
||||
return true
|
||||
}
|
||||
|
||||
func lookup_blocks_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbBlocks? {
|
||||
func lookup_blocks_by_key_with_txn(_ key: NoteKey, txn: RawNdbTxnAccessible) -> NdbBlockGroup.BlocksMetadata? {
|
||||
guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NdbBlocks(ptr: blocks)
|
||||
return NdbBlockGroup.BlocksMetadata(ptr: blocks)
|
||||
}
|
||||
|
||||
func lookup_blocks_by_key(_ key: NoteKey) -> NdbTxn<NdbBlocks?>? {
|
||||
NdbTxn(ndb: self) { txn in
|
||||
func lookup_blocks_by_key(_ key: NoteKey) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>? {
|
||||
SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.new(on: self) { txn in
|
||||
lookup_blocks_by_key_with_txn(key, txn: txn)
|
||||
}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ class Ndb {
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_note_key_with_txn<Y>(_ id: NoteId, txn: NdbTxn<Y>) -> NoteKey? {
|
||||
func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? {
|
||||
guard !closed else { return nil }
|
||||
return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in
|
||||
guard let p = ptr.baseAddress else {
|
||||
|
||||
@@ -93,31 +93,180 @@ enum NdbBlock {
|
||||
guard let cString = block.str else {
|
||||
return nil
|
||||
}
|
||||
// Copy byte-by-byte from the pointer into a new buffer
|
||||
let byteBuffer = UnsafeBufferPointer(start: cString, count: Int(block.len)).map { UInt8(bitPattern: $0) }
|
||||
|
||||
// Create a Swift String from the byte array
|
||||
// Create an owned Swift String from the buffer we created
|
||||
return String(bytes: byteBuffer, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NdbBlocks {
|
||||
private let blocks_ptr: ndb_blocks_ptr
|
||||
|
||||
init(ptr: OpaquePointer?) {
|
||||
self.blocks_ptr = ndb_blocks_ptr(ptr: ptr)
|
||||
/// Represents a group of blocks
|
||||
struct NdbBlockGroup: ~Copyable {
|
||||
/// The block offsets
|
||||
fileprivate let metadata: MaybeTxn<BlocksMetadata>
|
||||
/// The raw text content of the note
|
||||
fileprivate let rawTextContent: String
|
||||
/// An iterable list of blocks that make up this object
|
||||
var blocks: [NdbBlock] {
|
||||
return self.collectBlocks()
|
||||
}
|
||||
|
||||
var words: Int {
|
||||
Int(ndb_blocks_word_count(blocks_ptr.ptr))
|
||||
return metadata.borrow { $0.words }
|
||||
}
|
||||
|
||||
func iter(note: NdbNote) -> BlocksSequence {
|
||||
BlocksSequence(note: note, blocks: self)
|
||||
|
||||
/// Gets the parsed blocks from a specific note.
|
||||
///
|
||||
/// This function will:
|
||||
/// - fetch blocks information from NostrDB if possible _and_ available, or
|
||||
/// - parse blocks on-demand.
|
||||
static func from(event: NdbNote, using ndb: Ndb, and keypair: Keypair) throws(NdbBlocksError) -> Self {
|
||||
if event.is_content_encrypted() {
|
||||
return try parse(event: event, keypair: keypair)
|
||||
}
|
||||
else {
|
||||
guard let offsets = event.block_offsets(ndb: ndb) else {
|
||||
return try parse(event: event, keypair: keypair)
|
||||
}
|
||||
return .init(metadata: .txn(offsets), rawTextContent: event.content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the note contents on-demand from a specific note.
|
||||
///
|
||||
/// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible.
|
||||
static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self {
|
||||
guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError }
|
||||
guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError }
|
||||
return self.init(
|
||||
metadata: .pure(metadata),
|
||||
rawTextContent: content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func as_ptr() -> OpaquePointer? {
|
||||
return self.blocks_ptr.ptr
|
||||
enum MaybeTxn<T: ~Copyable>: ~Copyable {
|
||||
case pure(T)
|
||||
case txn(SafeNdbTxn<T>)
|
||||
|
||||
func borrow<Y>(_ borrowFunction: (borrowing T) -> Y) -> Y {
|
||||
switch self {
|
||||
case .pure(let item):
|
||||
return borrowFunction(item)
|
||||
case .txn(let txn):
|
||||
return borrowFunction(txn.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper structs
|
||||
|
||||
extension NdbBlockGroup {
|
||||
/// Wrapper for the `ndb_blocks` C struct
|
||||
///
|
||||
/// This does not store the actual block contents, only the offsets on the content string and block metadata.
|
||||
///
|
||||
/// **Implementation note:** This would be better as `~Copyable`, but `NdbTxn` does not support `~Copyable` yet.
|
||||
struct BlocksMetadata: ~Copyable {
|
||||
private let blocks_ptr: ndb_blocks_ptr
|
||||
private let buffer: UnsafeMutableRawPointer?
|
||||
|
||||
init(ptr: OpaquePointer?, buffer: UnsafeMutableRawPointer? = nil) {
|
||||
self.blocks_ptr = ndb_blocks_ptr(ptr: ptr)
|
||||
self.buffer = buffer
|
||||
}
|
||||
|
||||
var words: Int {
|
||||
Int(ndb_blocks_word_count(blocks_ptr.ptr))
|
||||
}
|
||||
|
||||
/// Gets the opaque pointer
|
||||
///
|
||||
/// **Implementation note:** This is marked `fileprivate` because we want to minimize the exposure of raw pointers to Swift code outside these wrapper structs.
|
||||
fileprivate func as_ptr() -> OpaquePointer? {
|
||||
return self.blocks_ptr.ptr
|
||||
}
|
||||
|
||||
/// Parses text content and returns the parsed block metadata if successful
|
||||
///
|
||||
/// **Implementation notes:** This is `fileprivate` because it makes no sense for outside Swift code to use this directly. Use `NdbBlockGroup` instead.
|
||||
fileprivate static func parseContent(content: String) -> Self? {
|
||||
// Allocate scratch buffer with enough space
|
||||
guard let buffer = malloc(MAX_NOTE_SIZE) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var blocks: OpaquePointer? = nil
|
||||
|
||||
// Call the C parsing function and check its success status
|
||||
let success = content.withCString { contentPtr -> Bool in
|
||||
let contentLen = content.utf8.count
|
||||
return ndb_parse_content(
|
||||
buffer.assumingMemoryBound(to: UInt8.self),
|
||||
Int32(MAX_NOTE_SIZE),
|
||||
contentPtr,
|
||||
Int32(contentLen),
|
||||
&blocks
|
||||
) == 1
|
||||
}
|
||||
|
||||
if !success || blocks == nil {
|
||||
// Something failed
|
||||
free(buffer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: We should set the owned flag as in the C code.
|
||||
// However, There does not seem to be a way to set this from Swift code. The code shown below does not work.
|
||||
// blocks!.pointee.flags |= NDB_BLOCK_FLAG_OWNED
|
||||
// But perhaps this is not necessary because `NdbBlockGroup` is non-copyable
|
||||
|
||||
return BlocksMetadata(ptr: blocks, buffer: buffer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let buffer {
|
||||
free(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Models specific errors that may happen when parsing or constructing an `NdbBlocks` object
|
||||
enum NdbBlocksError: Error {
|
||||
case parseError
|
||||
case decryptionError
|
||||
}
|
||||
}
|
||||
|
||||
extension NdbBlockGroup {
|
||||
/// Collects all blocks in the group into an array without using Iterator/Sequence protocols
|
||||
///
|
||||
/// **Implementation note:**
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it does seem to be possible to conform to both `Sequence` and `~Copyable` at the same time.
|
||||
///
|
||||
/// - Returns: An array of all blocks in the group
|
||||
fileprivate func collectBlocks() -> [NdbBlock] {
|
||||
var blocks = [NdbBlock]()
|
||||
|
||||
// Ensure the C string remains valid for the entire operation by keeping
|
||||
// all operations using it within the withCString closure
|
||||
self.rawTextContent.withCString { cptr in
|
||||
var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil)
|
||||
|
||||
// Start the iteration
|
||||
self.metadata.borrow { value in
|
||||
ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
|
||||
}
|
||||
|
||||
// Collect blocks into array
|
||||
while let ptr = ndb_blocks_iterate_next(&iter),
|
||||
let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
|
||||
blocks.append(block)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//
|
||||
// NdbBlockIterator.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2024-01-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct BlocksIterator: IteratorProtocol {
|
||||
typealias Element = NdbBlock
|
||||
|
||||
var done: Bool
|
||||
var iter: ndb_block_iterator
|
||||
var note: NdbNote
|
||||
|
||||
mutating func next() -> NdbBlock? {
|
||||
guard iter.blocks != nil,
|
||||
let ptr = ndb_blocks_iterate_next(&iter) else {
|
||||
done = true
|
||||
return nil
|
||||
}
|
||||
|
||||
let block_ptr = ndb_block_ptr(ptr: ptr)
|
||||
return NdbBlock(block_ptr)
|
||||
}
|
||||
|
||||
init(note: NdbNote, blocks: NdbBlocks) {
|
||||
let content = ndb_note_content(note.note.ptr)
|
||||
self.iter = ndb_block_iterator(content: content, blocks: nil, block: ndb_block(), p: nil)
|
||||
ndb_blocks_iterate_start(content, blocks.as_ptr(), &self.iter)
|
||||
self.done = false
|
||||
self.note = note
|
||||
}
|
||||
}
|
||||
|
||||
struct BlocksSequence: Sequence {
|
||||
let blocks: NdbBlocks
|
||||
let note: NdbNote
|
||||
|
||||
init(note: NdbNote, blocks: NdbBlocks) {
|
||||
self.blocks = blocks
|
||||
self.note = note
|
||||
}
|
||||
|
||||
func makeIterator() -> BlocksIterator {
|
||||
return .init(note: note, blocks: blocks)
|
||||
}
|
||||
|
||||
func collect() -> [NdbBlock] {
|
||||
var xs = [NdbBlock]()
|
||||
for x in self {
|
||||
xs.append(x)
|
||||
}
|
||||
return xs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,23 +457,25 @@ extension NdbNote {
|
||||
return ThreadReply(tags: self.tags)?.reply.note_id
|
||||
}
|
||||
|
||||
func blocks(ndb: Ndb) -> NdbTxn<NdbBlocks>? {
|
||||
let blocks_txn = NdbTxn<NdbBlocks?>(ndb: ndb) { txn in
|
||||
func block_offsets(ndb: Ndb) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? {
|
||||
let blocks_txn: SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? = .new(on: ndb) { txn -> NdbBlockGroup.BlocksMetadata? in
|
||||
guard let key = ndb.lookup_note_key_with_txn(self.id, txn: txn) else {
|
||||
return nil
|
||||
}
|
||||
return ndb.lookup_blocks_by_key_with_txn(key, txn: txn)
|
||||
}
|
||||
|
||||
guard let blocks_txn else {
|
||||
return nil
|
||||
}
|
||||
guard let blocks_txn else { return nil }
|
||||
|
||||
return blocks_txn.collect()
|
||||
return blocks_txn
|
||||
}
|
||||
|
||||
func is_content_encrypted() -> Bool {
|
||||
return known_kind == .dm // Probably other kinds should be listed here
|
||||
}
|
||||
|
||||
func get_content(_ keypair: Keypair) -> String {
|
||||
if known_kind == .dm {
|
||||
if is_content_encrypted() {
|
||||
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
||||
}
|
||||
else if known_kind == .highlight {
|
||||
@@ -484,7 +486,7 @@ extension NdbNote {
|
||||
}
|
||||
|
||||
func maybe_get_content(_ keypair: Keypair) -> String? {
|
||||
if known_kind == .dm {
|
||||
if is_content_encrypted() {
|
||||
return decrypted(keypair: keypair)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ fileprivate var txn_count: Int = 0
|
||||
#endif
|
||||
|
||||
// Would use struct and ~Copyable but generics aren't supported well
|
||||
class NdbTxn<T> {
|
||||
class NdbTxn<T>: RawNdbTxnAccessible {
|
||||
var txn: ndb_txn
|
||||
private var val: T!
|
||||
var moved: Bool
|
||||
@@ -117,6 +117,129 @@ class NdbTxn<T> {
|
||||
}
|
||||
}
|
||||
|
||||
protocol RawNdbTxnAccessible: AnyObject {
|
||||
var txn: ndb_txn { get set }
|
||||
}
|
||||
|
||||
class PlaceholderNdbTxn: RawNdbTxnAccessible {
|
||||
var txn: ndb_txn
|
||||
|
||||
init(txn: ndb_txn) {
|
||||
self.txn = txn
|
||||
}
|
||||
}
|
||||
|
||||
class SafeNdbTxn<T: ~Copyable> {
|
||||
var txn: ndb_txn
|
||||
var val: T!
|
||||
var moved: Bool
|
||||
var inherited: Bool
|
||||
var ndb: Ndb
|
||||
var generation: Int
|
||||
var name: String
|
||||
|
||||
static func pure(ndb: Ndb, val: consuming T) -> SafeNdbTxn<T> {
|
||||
.init(ndb: ndb, txn: ndb_txn(), val: val, generation: ndb.generation, inherited: true, name: "pure_txn")
|
||||
}
|
||||
|
||||
static func new(on ndb: Ndb, with valueGetter: (PlaceholderNdbTxn) -> T? = { _ in () }, name: String = "txn") -> SafeNdbTxn<T>? {
|
||||
guard !ndb.is_closed else { return nil }
|
||||
var generation = ndb.generation
|
||||
var txn: ndb_txn
|
||||
let inherited: Bool
|
||||
if let active_txn = Thread.current.threadDictionary["ndb_txn"] as? ndb_txn {
|
||||
// some parent thread is active, use that instead
|
||||
print("txn: inherited txn")
|
||||
txn = active_txn
|
||||
inherited = true
|
||||
generation = Thread.current.threadDictionary["txn_generation"] as! Int
|
||||
} else {
|
||||
txn = ndb_txn()
|
||||
guard !ndb.is_closed else { return nil }
|
||||
generation = ndb.generation
|
||||
#if TXNDEBUG
|
||||
txn_count += 1
|
||||
#endif
|
||||
let ok = ndb_begin_query(ndb.ndb.ndb, &txn) != 0
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
generation = ndb.generation
|
||||
Thread.current.threadDictionary["ndb_txn"] = txn
|
||||
Thread.current.threadDictionary["txn_generation"] = ndb.generation
|
||||
inherited = false
|
||||
}
|
||||
#if TXNDEBUG
|
||||
print("txn: open gen\(self.generation) '\(self.name)' \(txn_count)")
|
||||
#endif
|
||||
let moved = false
|
||||
let placeholderTxn = PlaceholderNdbTxn(txn: txn)
|
||||
guard let val = valueGetter(placeholderTxn) else { return nil }
|
||||
return SafeNdbTxn<T>(ndb: ndb, txn: txn, val: val, generation: generation, inherited: inherited, name: name)
|
||||
}
|
||||
|
||||
private init(ndb: Ndb, txn: ndb_txn, val: consuming T, generation: Int, inherited: Bool, name: String) {
|
||||
self.txn = txn
|
||||
self.val = consume val
|
||||
self.moved = false
|
||||
self.inherited = inherited
|
||||
self.ndb = ndb
|
||||
self.generation = generation
|
||||
self.name = name
|
||||
}
|
||||
|
||||
deinit {
|
||||
if self.generation != ndb.generation {
|
||||
print("txn: OLD GENERATION (\(self.generation) != \(ndb.generation)), IGNORING")
|
||||
return
|
||||
}
|
||||
if inherited {
|
||||
print("txn: not closing. inherited ")
|
||||
return
|
||||
}
|
||||
if moved {
|
||||
//print("txn: not closing. moved")
|
||||
return
|
||||
}
|
||||
if ndb.is_closed {
|
||||
print("txn: not closing. db closed")
|
||||
return
|
||||
}
|
||||
|
||||
#if TXNDEBUG
|
||||
txn_count -= 1;
|
||||
print("txn: close gen\(generation) '\(name)' \(txn_count)")
|
||||
#endif
|
||||
ndb_end_query(&self.txn)
|
||||
//self.skip_close = true
|
||||
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn")
|
||||
}
|
||||
|
||||
// functor
|
||||
func map<Y>(_ transform: (borrowing T) -> Y) -> SafeNdbTxn<Y> {
|
||||
self.moved = true
|
||||
return .init(ndb: self.ndb, txn: self.txn, val: transform(val), generation: generation, inherited: inherited, name: self.name)
|
||||
}
|
||||
|
||||
// comonad!?
|
||||
// useful for moving ownership of a transaction to another value
|
||||
func extend<Y>(_ with: (SafeNdbTxn<T>) -> Y) -> SafeNdbTxn<Y> {
|
||||
self.moved = true
|
||||
return .init(ndb: self.ndb, txn: self.txn, val: with(self), generation: generation, inherited: inherited, name: self.name)
|
||||
}
|
||||
|
||||
consuming func maybeExtend<Y>(_ with: (consuming SafeNdbTxn<T>) -> Y?) -> SafeNdbTxn<Y>? where Y: ~Copyable {
|
||||
self.moved = true
|
||||
let ndb = self.ndb
|
||||
let txn = self.txn
|
||||
let generation = self.generation
|
||||
let inherited = self.inherited
|
||||
let name = self.name
|
||||
guard let newVal = with(consume self) else { return nil }
|
||||
return .init(ndb: ndb, txn: txn, val: newVal, generation: generation, inherited: inherited, name: name)
|
||||
}
|
||||
}
|
||||
|
||||
protocol OptionalType {
|
||||
associatedtype Wrapped
|
||||
var optional: Wrapped? { get }
|
||||
|
||||
Reference in New Issue
Block a user