@@ -9,6 +9,8 @@ use anyhow::{Context, Result, anyhow};
99use chrono:: { DateTime , Utc } ;
1010use serde:: { Deserialize , Deserializer , Serialize , Serializer } ;
1111
12+ use crate :: pg:: parse_hex;
13+
1214pub mod copy;
1315pub mod delete;
1416pub mod delta;
@@ -107,20 +109,17 @@ pub fn format_backup_name(timeline: u32, start_lsn: u64, seg_size: u64) -> Strin
107109/// the prefix, is too short, or contains non-hex digits.
108110pub fn parse_timeline_from_backup_name ( name : & str ) -> Option < u32 > {
109111 let rest = name. strip_prefix ( BACKUP_NAME_PREFIX ) ?;
110- if rest. len ( ) < 8 {
111- return None ;
112- }
113- u32:: from_str_radix ( & rest[ ..8 ] , 16 ) . ok ( )
112+ Some ( parse_hex ( rest. as_bytes ( ) . get ( ..8 ) ?, 8 ) ? as u32 )
114113}
115114
116- /// Parse `0/1A2B3C4D` (postgres pg_lsn text form) into u64
115+ /// Parse `0/1A2B3C4D` (postgres pg_lsn text form) into u64.
116+ /// Strict per pg_lsn_in_internal: 1..=8 hex digits per component, no sign
117117pub fn parse_pg_lsn ( s : & str ) -> Result < u64 > {
118118 let s = s. trim ( ) ;
119119 let ( hi, lo) = s
120120 . split_once ( '/' )
121+ . and_then ( |( hi, lo) | parse_hex ( hi. as_bytes ( ) , 8 ) . zip ( parse_hex ( lo. as_bytes ( ) , 8 ) ) )
121122 . ok_or_else ( || anyhow ! ( "bad LSN format: {s}" ) ) ?;
122- let hi = u64:: from_str_radix ( hi, 16 ) . with_context ( || format ! ( "bad LSN hi: {hi}" ) ) ?;
123- let lo = u64:: from_str_radix ( lo, 16 ) . with_context ( || format ! ( "bad LSN lo: {lo}" ) ) ?;
124123 Ok ( ( hi << 32 ) | lo)
125124}
126125
@@ -139,13 +138,9 @@ pub fn format_pg_lsn(lsn: u64) -> impl std::fmt::Display {
139138
140139/// Match `base_<24hex>` and optional `_D_<24hex>` delta and `_<8hex>` LSN
141140pub fn looks_like_backup_name ( s : & str ) -> bool {
142- let Some ( rest) = s. strip_prefix ( BACKUP_NAME_PREFIX ) else {
143- return false ;
144- } ;
145- if rest. len ( ) < 24 {
146- return false ;
147- }
148- rest[ ..24 ] . chars ( ) . all ( |c| c. is_ascii_hexdigit ( ) )
141+ s. strip_prefix ( BACKUP_NAME_PREFIX )
142+ . and_then ( |rest| rest. as_bytes ( ) . get ( ..24 ) )
143+ . is_some_and ( |w| w. iter ( ) . all ( u8:: is_ascii_hexdigit) )
149144}
150145
151146/// Strip wal-g sentinel suffix to recover backup name
@@ -566,6 +561,42 @@ mod tests {
566561 assert_eq ! ( parse_pg_lsn( "FF/FF" ) . unwrap( ) , ( 0xFF_u64 << 32 ) | 0xFF ) ;
567562 }
568563
564+ #[ test]
565+ fn rejects_malformed_lsn ( ) {
566+ for s in [
567+ "" ,
568+ "1" ,
569+ "/" ,
570+ "1/" ,
571+ "/1" ,
572+ "0x1/0" ,
573+ "+1/0" ,
574+ "1/+0" ,
575+ "-1/0" ,
576+ "000000001/0" , // 9 digits, pg caps components at 8
577+ "0/000000001" ,
578+ "1FFFFFFFF/0" , // hi overflow must not shift into oblivion
579+ "0/1FFFFFFFF" , // lo overflow must not bleed into hi
580+ "1 /0" ,
581+ "0/ 1" ,
582+ "g/0" ,
583+ "1/2/3" ,
584+ ] {
585+ assert ! ( parse_pg_lsn( s) . is_err( ) , "{s:?} should be rejected" ) ;
586+ }
587+ }
588+
589+ #[ test]
590+ fn backup_name_parsers_reject_sign_and_multibyte ( ) {
591+ assert_eq ! ( parse_timeline_from_backup_name( "base_+0000001rest" ) , None ) ;
592+ // char straddling the 8- and 24-byte windows: must not panic
593+ assert_eq ! ( parse_timeline_from_backup_name( "base_0000000é0" ) , None ) ;
594+ assert ! ( !looks_like_backup_name( & format!(
595+ "base_{}é" ,
596+ "0" . repeat( 23 )
597+ ) ) ) ;
598+ }
599+
569600 #[ test]
570601 fn formats_lsn_uppercase ( ) {
571602 assert_eq ! ( format_pg_lsn( 0x0300_0000 ) . to_string( ) , "0/3000000" ) ;
0 commit comments