@@ -275,12 +275,14 @@ pub struct Configuration {
275275
276276 /// Sender micro-session member link ID for LAG measurement (RFC 9534).
277277 /// When set, includes a Micro-session ID TLV in test packets.
278- #[ clap( long, value_parser = clap:: value_parser!( u16 ) . range( 1 ..) ) ]
278+ /// Accepts decimal (e.g. `255`) or `0x`-prefixed hex (e.g. `0xff`).
279+ #[ clap( long, value_parser = parse_u16_nonzero_dec_or_hex) ]
279280 pub micro_session_id : Option < u16 > ,
280281
281282 /// Reflector member link ID for LAG micro-sessions (RFC 9534).
282283 /// When set, the reflector fills this ID into reflected Micro-session ID TLVs.
283- #[ clap( long, value_parser = clap:: value_parser!( u16 ) . range( 1 ..) ) ]
284+ /// Accepts decimal (e.g. `171`) or `0x`-prefixed hex (e.g. `0xab`).
285+ #[ clap( long, value_parser = parse_u16_nonzero_dec_or_hex) ]
284286 pub reflector_member_link_id : Option < u16 > ,
285287
286288 /// Maximum packets per second per source (0 = unlimited).
@@ -706,6 +708,29 @@ pub fn is_auth(mode: AuthMode) -> bool {
706708 mode. is_authenticated ( )
707709}
708710
711+ /// clap value_parser: parse a u16 from decimal or `0x`-prefixed hex, rejecting 0.
712+ ///
713+ /// Accepts: `255`, `0xff`, `0XFF`, `0x00ab`. Rejects: `0`, `0x0`, empty, `ff`,
714+ /// out-of-range. Used by LAG identifier flags where the RFC 9534 wire field is
715+ /// commonly written in hex.
716+ fn parse_u16_nonzero_dec_or_hex ( s : & str ) -> Result < u16 , String > {
717+ let trimmed = s. trim ( ) ;
718+ let parsed = if let Some ( rest) = trimmed
719+ . strip_prefix ( "0x" )
720+ . or_else ( || trimmed. strip_prefix ( "0X" ) )
721+ {
722+ u16:: from_str_radix ( rest, 16 ) . map_err ( |e| format ! ( "invalid hex value `{s}`: {e}" ) ) ?
723+ } else {
724+ trimmed
725+ . parse :: < u16 > ( )
726+ . map_err ( |e| format ! ( "invalid value `{s}`: {e}" ) ) ?
727+ } ;
728+ if parsed == 0 {
729+ return Err ( format ! ( "value `{s}` must be in range 1..=65535" ) ) ;
730+ }
731+ Ok ( parsed)
732+ }
733+
709734#[ cfg( test) ]
710735mod tests {
711736 use clap:: Parser ;
@@ -1542,6 +1567,80 @@ mod tests {
15421567 assert ! ( err. to_string( ) . contains( "reflector_member_link_id" ) ) ;
15431568 }
15441569
1570+ #[ test]
1571+ fn test_parse_u16_nonzero_dec_or_hex_accepts_decimal ( ) {
1572+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "1" ) . unwrap( ) , 1 ) ;
1573+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "255" ) . unwrap( ) , 255 ) ;
1574+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "65535" ) . unwrap( ) , 65535 ) ;
1575+ }
1576+
1577+ #[ test]
1578+ fn test_parse_u16_nonzero_dec_or_hex_accepts_hex ( ) {
1579+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0x1" ) . unwrap( ) , 1 ) ;
1580+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0xff" ) . unwrap( ) , 255 ) ;
1581+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0xFF" ) . unwrap( ) , 255 ) ;
1582+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0X00ab" ) . unwrap( ) , 0xab ) ;
1583+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0xffff" ) . unwrap( ) , 65535 ) ;
1584+ }
1585+
1586+ #[ test]
1587+ fn test_parse_u16_nonzero_dec_or_hex_rejects_zero ( ) {
1588+ assert ! ( parse_u16_nonzero_dec_or_hex( "0" ) . is_err( ) ) ;
1589+ assert ! ( parse_u16_nonzero_dec_or_hex( "0x0" ) . is_err( ) ) ;
1590+ assert ! ( parse_u16_nonzero_dec_or_hex( "0x0000" ) . is_err( ) ) ;
1591+ }
1592+
1593+ #[ test]
1594+ fn test_parse_u16_nonzero_dec_or_hex_rejects_garbage ( ) {
1595+ assert ! ( parse_u16_nonzero_dec_or_hex( "" ) . is_err( ) ) ;
1596+ assert ! ( parse_u16_nonzero_dec_or_hex( "ff" ) . is_err( ) ) ; // hex without 0x prefix
1597+ assert ! ( parse_u16_nonzero_dec_or_hex( "0x1g" ) . is_err( ) ) ;
1598+ assert ! ( parse_u16_nonzero_dec_or_hex( "0x10000" ) . is_err( ) ) ; // > u16::MAX
1599+ assert ! ( parse_u16_nonzero_dec_or_hex( "65536" ) . is_err( ) ) ;
1600+ // Empty string after stripping `0x` prefix → from_str_radix rejects.
1601+ assert ! ( parse_u16_nonzero_dec_or_hex( "0x" ) . is_err( ) ) ;
1602+ assert ! ( parse_u16_nonzero_dec_or_hex( "0X" ) . is_err( ) ) ;
1603+ }
1604+
1605+ #[ test]
1606+ fn test_parse_u16_nonzero_dec_or_hex_handles_whitespace ( ) {
1607+ // clap doesn't usually pass whitespace, but the parser trims defensively
1608+ // (e.g. when values are loaded from the TOML config file).
1609+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( " 0xff" ) . unwrap( ) , 0xff ) ;
1610+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "0xff " ) . unwrap( ) , 0xff ) ;
1611+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( " 255 " ) . unwrap( ) , 255 ) ;
1612+ assert_eq ! ( parse_u16_nonzero_dec_or_hex( "\t 0x1\n " ) . unwrap( ) , 1 ) ;
1613+ }
1614+
1615+ #[ test]
1616+ fn test_micro_session_id_accepts_hex_on_cli ( ) {
1617+ let conf = load_from_args ( & [
1618+ "test" ,
1619+ "--remote-addr" ,
1620+ "127.0.0.1" ,
1621+ "--micro-session-id" ,
1622+ "0xff" ,
1623+ "--reflector-member-link-id" ,
1624+ "0xab" ,
1625+ ] )
1626+ . unwrap ( ) ;
1627+ assert_eq ! ( conf. micro_session_id, Some ( 0xff ) ) ;
1628+ assert_eq ! ( conf. reflector_member_link_id, Some ( 0xab ) ) ;
1629+ }
1630+
1631+ #[ test]
1632+ fn test_micro_session_id_accepts_decimal_on_cli ( ) {
1633+ let conf = load_from_args ( & [
1634+ "test" ,
1635+ "--remote-addr" ,
1636+ "127.0.0.1" ,
1637+ "--micro-session-id" ,
1638+ "255" ,
1639+ ] )
1640+ . unwrap ( ) ;
1641+ assert_eq ! ( conf. micro_session_id, Some ( 255 ) ) ;
1642+ }
1643+
15451644 #[ test]
15461645 fn test_validate_rejects_return_path_cc_with_sr_mpls_from_file ( ) {
15471646 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments