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:
Daniel D’Aquino
2025-06-27 19:27:42 -07:00
parent 9c47d2e0bd
commit caa7802bce
11 changed files with 350 additions and 129 deletions

View File

@@ -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
}
}