@@ -989,7 +989,14 @@ impl VerifiedInvoiceRequest {
989989 InvoiceWithDerivedSigningPubkeyBuilder
990990 ) ;
991991
992- pub ( crate ) fn fields ( & self ) -> InvoiceRequestFields {
992+ /// Fetch the [`InvoiceRequestFields`] for this verified invoice.
993+ ///
994+ /// These are fields which we expect to be useful when receiving a payment for this invoice
995+ /// request, and include the returned [`InvoiceRequestFields`] in the
996+ /// [`PaymentContext::Bolt12Offer`].
997+ ///
998+ /// [`PaymentContext::Bolt12Offer`]: crate::blinded_path::payment::PaymentContext::Bolt12Offer
999+ pub fn fields ( & self ) -> InvoiceRequestFields {
9931000 let InvoiceRequestContents {
9941001 payer_signing_pubkey,
9951002 inner : InvoiceRequestContentsWithoutPayerSigningPubkey { quantity, payer_note, .. } ,
@@ -998,15 +1005,37 @@ impl VerifiedInvoiceRequest {
9981005 InvoiceRequestFields {
9991006 payer_signing_pubkey : * payer_signing_pubkey,
10001007 quantity : * quantity,
1001- payer_note_truncated : payer_note. clone ( ) . map ( |mut s| {
1002- s. truncate ( PAYER_NOTE_LIMIT ) ;
1003- UntrustedString ( s)
1004- } ) ,
1008+ payer_note_truncated : payer_note
1009+ . clone ( )
1010+ // Truncate the payer note to `PAYER_NOTE_LIMIT` bytes, rounding
1011+ // down to the nearest valid UTF-8 code point boundary.
1012+ . map ( |s| UntrustedString ( string_truncate_safe ( s, PAYER_NOTE_LIMIT ) ) ) ,
10051013 human_readable_name : self . offer_from_hrn ( ) . clone ( ) ,
10061014 }
10071015 }
10081016}
10091017
1018+ /// `String::truncate(new_len)` panics if you split inside a UTF-8 code point,
1019+ /// which would leave the `String` containing invalid UTF-8. This function will
1020+ /// instead truncate the string to the next smaller code point boundary so the
1021+ /// truncated string always remains valid UTF-8.
1022+ ///
1023+ /// This can still split a grapheme cluster, but that's probably fine.
1024+ /// We'd otherwise have to pull in the `unicode-segmentation` crate and its big
1025+ /// unicode tables to find the next smaller grapheme cluster boundary.
1026+ fn string_truncate_safe ( mut s : String , new_len : usize ) -> String {
1027+ // Finds the largest byte index `x` not exceeding byte index `index` where
1028+ // `s.is_char_boundary(x)` is true.
1029+ // TODO(phlip9): remove when `std::str::floor_char_boundary` stabilizes.
1030+ let truncated_len = if new_len >= s. len ( ) {
1031+ s. len ( )
1032+ } else {
1033+ ( 0 ..=new_len) . rev ( ) . find ( |idx| s. is_char_boundary ( * idx) ) . unwrap_or ( 0 )
1034+ } ;
1035+ s. truncate ( truncated_len) ;
1036+ s
1037+ }
1038+
10101039impl InvoiceRequestContents {
10111040 pub ( super ) fn metadata ( & self ) -> & [ u8 ] {
10121041 self . inner . metadata ( )
@@ -1382,8 +1411,13 @@ pub struct InvoiceRequestFields {
13821411}
13831412
13841413/// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
1414+ #[ cfg( not( fuzzing) ) ]
13851415pub const PAYER_NOTE_LIMIT : usize = 512 ;
13861416
1417+ /// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
1418+ #[ cfg( fuzzing) ]
1419+ pub const PAYER_NOTE_LIMIT : usize = 8 ;
1420+
13871421impl Writeable for InvoiceRequestFields {
13881422 fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
13891423 write_tlv_fields ! ( writer, {
@@ -1426,6 +1460,7 @@ mod tests {
14261460 use crate :: ln:: inbound_payment:: ExpandedKey ;
14271461 use crate :: ln:: msgs:: { DecodeError , MAX_VALUE_MSAT } ;
14281462 use crate :: offers:: invoice:: { Bolt12Invoice , SIGNATURE_TAG as INVOICE_SIGNATURE_TAG } ;
1463+ use crate :: offers:: invoice_request:: string_truncate_safe;
14291464 use crate :: offers:: merkle:: { self , SignatureTlvStreamRef , TaggedHash , TlvStream } ;
14301465 use crate :: offers:: nonce:: Nonce ;
14311466 #[ cfg( not( c_bindings) ) ]
@@ -2947,14 +2982,20 @@ mod tests {
29472982 . unwrap ( ) ;
29482983 assert_eq ! ( offer. issuer_signing_pubkey( ) , Some ( node_id) ) ;
29492984
2985+ // UTF-8 payer note that we can't naively `.truncate(PAYER_NOTE_LIMIT)`
2986+ // because it would split a multi-byte UTF-8 code point.
2987+ let payer_note = "❤️" . repeat ( 86 ) ;
2988+ assert_eq ! ( payer_note. len( ) , PAYER_NOTE_LIMIT + 4 ) ;
2989+ let expected_payer_note = "❤️" . repeat ( 85 ) ;
2990+
29502991 let invoice_request = offer
29512992 . request_invoice ( & expanded_key, nonce, & secp_ctx, payment_id)
29522993 . unwrap ( )
29532994 . chain ( Network :: Testnet )
29542995 . unwrap ( )
29552996 . quantity ( 1 )
29562997 . unwrap ( )
2957- . payer_note ( "0" . repeat ( PAYER_NOTE_LIMIT * 2 ) )
2998+ . payer_note ( payer_note )
29582999 . build_and_sign ( )
29593000 . unwrap ( ) ;
29603001 match invoice_request. verify_using_metadata ( & expanded_key, & secp_ctx) {
@@ -2966,7 +3007,7 @@ mod tests {
29663007 InvoiceRequestFields {
29673008 payer_signing_pubkey: invoice_request. payer_signing_pubkey( ) ,
29683009 quantity: Some ( 1 ) ,
2969- payer_note_truncated: Some ( UntrustedString ( "0" . repeat ( PAYER_NOTE_LIMIT ) ) ) ,
3010+ payer_note_truncated: Some ( UntrustedString ( expected_payer_note ) ) ,
29703011 human_readable_name: None ,
29713012 }
29723013 ) ;
@@ -2981,4 +3022,31 @@ mod tests {
29813022 Err ( _) => panic ! ( "unexpected error" ) ,
29823023 }
29833024 }
3025+
3026+ #[ test]
3027+ fn test_string_truncate_safe ( ) {
3028+ // We'll correctly truncate to the nearest UTF-8 code point boundary:
3029+ // ❤ variation-selector
3030+ // e29da4 efb88f
3031+ let s = String :: from ( "❤️" ) ;
3032+ assert_eq ! ( s. len( ) , 6 ) ;
3033+ assert_eq ! ( s, string_truncate_safe( s. clone( ) , 7 ) ) ;
3034+ assert_eq ! ( s, string_truncate_safe( s. clone( ) , 6 ) ) ;
3035+ assert_eq ! ( "❤" , string_truncate_safe( s. clone( ) , 5 ) ) ;
3036+ assert_eq ! ( "❤" , string_truncate_safe( s. clone( ) , 4 ) ) ;
3037+ assert_eq ! ( "❤" , string_truncate_safe( s. clone( ) , 3 ) ) ;
3038+ assert_eq ! ( "" , string_truncate_safe( s. clone( ) , 2 ) ) ;
3039+ assert_eq ! ( "" , string_truncate_safe( s. clone( ) , 1 ) ) ;
3040+ assert_eq ! ( "" , string_truncate_safe( s. clone( ) , 0 ) ) ;
3041+
3042+ // Every byte in an ASCII string is also a full UTF-8 code point.
3043+ let s = String :: from ( "my ASCII string!" ) ;
3044+ for new_len in 0 ..( s. len ( ) + 5 ) {
3045+ if new_len >= s. len ( ) {
3046+ assert_eq ! ( s, string_truncate_safe( s. clone( ) , new_len) ) ;
3047+ } else {
3048+ assert_eq ! ( s[ ..new_len] , string_truncate_safe( s. clone( ) , new_len) ) ;
3049+ }
3050+ }
3051+ }
29843052}
0 commit comments