@@ -989,7 +989,14 @@ impl VerifiedInvoiceRequest {
989
989
InvoiceWithDerivedSigningPubkeyBuilder
990
990
) ;
991
991
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 {
993
1000
let InvoiceRequestContents {
994
1001
payer_signing_pubkey,
995
1002
inner : InvoiceRequestContentsWithoutPayerSigningPubkey { quantity, payer_note, .. } ,
@@ -998,15 +1005,37 @@ impl VerifiedInvoiceRequest {
998
1005
InvoiceRequestFields {
999
1006
payer_signing_pubkey : * payer_signing_pubkey,
1000
1007
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 ) ) ) ,
1005
1013
human_readable_name : self . offer_from_hrn ( ) . clone ( ) ,
1006
1014
}
1007
1015
}
1008
1016
}
1009
1017
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
+
1010
1039
impl InvoiceRequestContents {
1011
1040
pub ( super ) fn metadata ( & self ) -> & [ u8 ] {
1012
1041
self . inner . metadata ( )
@@ -1382,8 +1411,13 @@ pub struct InvoiceRequestFields {
1382
1411
}
1383
1412
1384
1413
/// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
1414
+ #[ cfg( not( fuzzing) ) ]
1385
1415
pub const PAYER_NOTE_LIMIT : usize = 512 ;
1386
1416
1417
+ /// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
1418
+ #[ cfg( fuzzing) ]
1419
+ pub const PAYER_NOTE_LIMIT : usize = 8 ;
1420
+
1387
1421
impl Writeable for InvoiceRequestFields {
1388
1422
fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > {
1389
1423
write_tlv_fields ! ( writer, {
@@ -1426,6 +1460,7 @@ mod tests {
1426
1460
use crate :: ln:: inbound_payment:: ExpandedKey ;
1427
1461
use crate :: ln:: msgs:: { DecodeError , MAX_VALUE_MSAT } ;
1428
1462
use crate :: offers:: invoice:: { Bolt12Invoice , SIGNATURE_TAG as INVOICE_SIGNATURE_TAG } ;
1463
+ use crate :: offers:: invoice_request:: string_truncate_safe;
1429
1464
use crate :: offers:: merkle:: { self , SignatureTlvStreamRef , TaggedHash , TlvStream } ;
1430
1465
use crate :: offers:: nonce:: Nonce ;
1431
1466
#[ cfg( not( c_bindings) ) ]
@@ -2947,14 +2982,20 @@ mod tests {
2947
2982
. unwrap ( ) ;
2948
2983
assert_eq ! ( offer. issuer_signing_pubkey( ) , Some ( node_id) ) ;
2949
2984
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
+
2950
2991
let invoice_request = offer
2951
2992
. request_invoice ( & expanded_key, nonce, & secp_ctx, payment_id)
2952
2993
. unwrap ( )
2953
2994
. chain ( Network :: Testnet )
2954
2995
. unwrap ( )
2955
2996
. quantity ( 1 )
2956
2997
. unwrap ( )
2957
- . payer_note ( "0" . repeat ( PAYER_NOTE_LIMIT * 2 ) )
2998
+ . payer_note ( payer_note )
2958
2999
. build_and_sign ( )
2959
3000
. unwrap ( ) ;
2960
3001
match invoice_request. verify_using_metadata ( & expanded_key, & secp_ctx) {
@@ -2966,7 +3007,7 @@ mod tests {
2966
3007
InvoiceRequestFields {
2967
3008
payer_signing_pubkey: invoice_request. payer_signing_pubkey( ) ,
2968
3009
quantity: Some ( 1 ) ,
2969
- payer_note_truncated: Some ( UntrustedString ( "0" . repeat ( PAYER_NOTE_LIMIT ) ) ) ,
3010
+ payer_note_truncated: Some ( UntrustedString ( expected_payer_note ) ) ,
2970
3011
human_readable_name: None ,
2971
3012
}
2972
3013
) ;
@@ -2981,4 +3022,31 @@ mod tests {
2981
3022
Err ( _) => panic ! ( "unexpected error" ) ,
2982
3023
}
2983
3024
}
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
+ }
2984
3052
}
0 commit comments