22# SPDX-License-Identifier: Apache-2.0
33
44import re
5+ import string
56import struct
67from typing import Any , List , Optional , Tuple , Union
78
89from elftools .elf .elffile import ELFFile
910
1011from .output_helpers import warning_print
1112
12- # Examples of format:
13- # %d - specifier='d'
14- # %10d - width='10', specifier='d'
15- # %-5.2f - flags='-', width='5', precision='2', specifier='f'
16- # %#08x - flags='#0', width='8', specifier='x'
17- # %llX - length='ll', specifier='X'
18- # %zu - length='z', specifier='u'
19- # %p - specifier='p'
20- PRINTF_FORMAT_REGEX = re .compile (
21- r'%(?P<flags>[-+0# ]*)?' # (1) Flags: Optional, can include '-', '+', '0', '#', or ' ' (space)
22- r'(?P<width>\*|\d+)?' # (2) Width: Optional, specifies minimum field width (e.g., "10" in "%10d")
23- r'(\.(?P<precision>\*|\d+))?' # (3) Precision: Optional, starts with '.', followed by digits (e.g., ".2" in "%.2f")
24- r'(?P<length>hh|h|l|ll|z|j|t|L)?' # (4) Length Modifier: Optional (e.g., "ll" in "%lld", "z" in "%zu")
25- r'(?P<specifier>[diuoxXfFeEgGaAcsp])' # (5) Specifier: Required (e.g., "d" for integers, "s" for strings)
26- )
27-
2813
2914class Control :
3015 FORMAT = '>H'
@@ -129,11 +114,14 @@ def retrieve_arguments(self, format: str, raw_args: bytes) -> List[Union[int, st
129114 args : List [Union [int , str , float , bytes ]] = []
130115 i_str = 0
131116 i_arg = 0
117+ arg_formatter = ArgFormatter ()
132118 while i_str < len (format ):
133- match = PRINTF_FORMAT_REGEX .search (format , i_str )
119+ match = arg_formatter . c_format_regex .search (format , i_str )
134120 if not match :
135121 break
136122 i_str = match .end ()
123+ if match .group (0 ) == '%%' :
124+ continue
137125 length = match .group ('length' ) or ''
138126 specifier = match .group ('specifier' )
139127
@@ -263,79 +251,8 @@ def convert_to_text(self, data: bytes) -> Tuple[List[bytes], bytes]:
263251 messages .append (self .format_message (msg ))
264252 return messages , incomplete_fragment
265253
266- @staticmethod
267- def convert_c_format_to_pythonic (fmt : str ) -> str :
268- """Convert C printf-style % formatting to Python {} formatting using a common regex."""
269-
270- def replace_match (m ):
271- """Helper function to convert printf specifiers to Python format."""
272- flags = m .group ('flags' ) or ''
273- width = m .group ('width' ) or ''
274- precision = m .group ('precision' ) or ''
275- specifier = m .group ('specifier' )
276-
277- # Convert printf flags to Python equivalents
278- python_flags = ''
279- if '-' in flags :
280- python_flags += '<' # Left-align
281- elif '0' in flags :
282- python_flags += '0' # Zero-padding
283- if '+' in flags :
284- python_flags += '+' # Force sign
285- elif ' ' in flags :
286- python_flags += ' ' # Space before positive numbers
287- if '#' in flags and specifier in 'oxX' : # Ensure correct alternate form
288- python_flags += '#'
289- if width and specifier == 'o' : # If width is specified, increase it by 1 to compensate for `0o` -> `0`
290- width = str (int (width ) + 1 )
291-
292- # Convert precision for integers (`%.5d` -> `{:05d}`)
293- if precision and specifier in 'diouxX' :
294- width = precision # Precision becomes width for zero-padding
295- python_flags = '0' # Force zero-padding
296- precision = None # Remove precision (Python does not support it for ints)
297-
298- # Handle width (`*` becomes `{}` placeholder)
299- width_placeholder = '*' if width == '*' else width
300-
301- # Convert specifier
302- python_specifier = specifier
303- if specifier in 'diu' : # Integer
304- python_specifier = 'd'
305- elif specifier in 'o' : # Octal
306- python_specifier = 'o'
307- elif specifier in 'xX' : # Hexadecimal
308- python_specifier = 'x' if specifier == 'x' else 'X'
309- elif specifier in 'fFeEgGaA' : # Floating-point
310- python_specifier = specifier .lower ()
311- elif specifier in 'c' : # Character
312- python_specifier = 's' # Convert `%c` to `{s}` (needs manual conversion)
313- elif specifier in 's' : # String
314- python_specifier = 's'
315- elif specifier in 'p' : # Pointer
316- python_specifier = '#x'
317-
318- # Construct final Python format specifier
319- python_format = '{:' + python_flags
320- if width_placeholder :
321- python_format += width_placeholder
322- python_format += python_specifier + '}'
323-
324- return python_format
325-
326- # Convert printf format specifiers to Python format specifiers
327- return PRINTF_FORMAT_REGEX .sub (replace_match , fmt )
328-
329- @staticmethod
330- def post_process_pythonic_format (formatted_message : str ) -> str :
331- """Fix specific formatting issues after conversion."""
332- # Fix octal formatting (`0o377` → `0377`)
333- formatted_message = formatted_message .replace ('0o' , '0' )
334- return formatted_message
335-
336254 def format_message (self , message : Message ) -> bytes :
337- text_msg = self .convert_c_format_to_pythonic (message .format ).format (* message .args )
338- text_msg = self .post_process_pythonic_format (text_msg )
255+ text_msg = ArgFormatter ().c_format (message .format , message .args )
339256 level_name = {1 : 'E' , 2 : 'W' , 3 : 'I' , 4 : 'D' , 5 : 'V' }[message .control .level ]
340257 return f'{ level_name } ({ message .timestamp } ) { message .tag } : { text_msg } \n ' .encode ('ascii' )
341258
@@ -371,7 +288,7 @@ def format_buffer_message(self, message) -> List[bytes]:
371288 # I (1024) log_example: 0x3ffb5bd0 74 61 72 74 65 64 20 69 73 20 74 6f 20 71 75 69 |tarted is to qui|
372289 while buff_len > 0 :
373290 tmp_len = min (BYTES_PER_LINE , buff_len )
374- hex_part = ' ' .join (f'{ b :02X } ' for b in buffer [:tmp_len ])
291+ hex_part = ' ' .join (f'{ b :02x } ' for b in buffer [:tmp_len ])
375292 hex_part_split = ' ' .join ([hex_part [:24 ], hex_part [24 :]])
376293 char_part = '' .join (chr (b ) if 32 <= b < 127 else '.' for b in buffer [:tmp_len ])
377294 message .format = f'0x{ buffer_addr :08x} { hex_part_split :<48} |{ char_part } |'
@@ -381,3 +298,117 @@ def format_buffer_message(self, message) -> List[bytes]:
381298 buff_len -= tmp_len
382299
383300 return text_msg
301+
302+
303+ class ArgFormatter (string .Formatter ):
304+ def __init__ (self ) -> None :
305+ # Examples of format:
306+ # %d - specifier='d'
307+ # %10d - width='10', specifier='d'
308+ # %-5.2f - flags='-', width='5', precision='2', specifier='f'
309+ # %#08x - flags='#0', width='8', specifier='x'
310+ # %llX - length='ll', specifier='X'
311+ # %zu - length='z', specifier='u'
312+ # %p - specifier='p'
313+ self .c_format_regex = re .compile (
314+ r'%%|' # (0) Match literal %%
315+ r'%(?P<flags>[-+0# ]*)?' # (1) Flags: Optional, can include '-', '+', '0', '#', or ' ' (space)
316+ r'(?P<width>\*|\d+)?' # (2) Width: Optional, specifies minimum field width (e.g., "10" in "%10d")
317+ r'(\.(?P<precision>\*|\d+))?' # (3) Precision: Optional, starts with '.', followed by digits (e.g., ".2" in "%.2f")
318+ r'(?P<length>hh|h|l|ll|z|j|t|L)?' # (4) Length Modifier: Optional (e.g., "ll" in "%lld", "z" in "%zu")
319+ r'(?P<specifier>[diuoxXfFeEgGaAcsp])' # (5) Specifier: Required (e.g., "d" for integers, "s" for strings)
320+ )
321+
322+ def format_field (self , value : Any , format_spec : str ) -> Any :
323+ if 'o' in format_spec and '#' in format_spec :
324+ # Fix octal formatting (`0o377` → `0377`)
325+ value = '0' + format (value , 'o' ) # Correct prefix for C-style octal
326+ format_spec = format_spec .replace ('o' , 's' ).replace ('#' , '' ) # Remove '#' and replace 'o' with 's'
327+ format_spec = ('>' if '<' not in format_spec else '' ) + format_spec
328+ return super ().format_field (value , format_spec )
329+
330+ def convert_to_pythonic_format (self , match : re .Match ) -> str :
331+ """Convert C-style format to Python-style and return the Python-style format string."""
332+ if not match :
333+ return ''
334+ if match .group (0 ) == '%%' :
335+ return '%'
336+ flags , width , precision , specifier = (
337+ match .group ('flags' ) or '' ,
338+ match .group ('width' ) or '' ,
339+ match .group ('precision' ) or '' ,
340+ match .group ('specifier' ),
341+ )
342+
343+ # Convert C-style flags to Python equivalents
344+ py_flags = self .convert_flags (flags , specifier )
345+ py_precision = ''
346+ if precision :
347+ # Convert precision for integers (`%.5d` -> `{:05d}`)
348+ if specifier in 'diouxX' :
349+ width = precision # Precision becomes width for zero-padding
350+ py_flags = '0' # Force zero-padding
351+ precision = None # Remove precision (Python does not support it for ints)
352+ else :
353+ py_precision = '.' + precision
354+ py_specifier = self .convert_specifier (specifier )
355+ py_width = width
356+
357+ # Build Python format specifier
358+ return '{:' + py_flags + py_width + py_precision + py_specifier + '}'
359+
360+ def convert_flags (self , flags : str , specifier : str ) -> str :
361+ """Convert C-style flags to Python format specifier flags."""
362+ py_flags = ''
363+ if specifier in 'sS' : # String
364+ if '-' not in flags :
365+ py_flags += '>'
366+ if '-' in flags :
367+ py_flags += '<' # Left-align
368+ elif '0' in flags :
369+ py_flags += '0' # Zero-padding
370+ if '+' in flags :
371+ py_flags += '+' # Force sign
372+ elif ' ' in flags :
373+ py_flags += ' ' # Space before positive numbers
374+ if '#' in flags and specifier in 'oxX' : # Alternate form for octal/hex
375+ py_flags += '#'
376+
377+ return py_flags
378+
379+ def convert_specifier (self , specifier : str ) -> str :
380+ """Convert C-style specifier to Python equivalent."""
381+ if specifier in 'diu' :
382+ return 'd'
383+ elif specifier == 'o' :
384+ return 'o'
385+ elif specifier in 'xX' :
386+ return 'x' if specifier == 'x' else 'X'
387+ elif specifier in 'fFeEgGaA' :
388+ return specifier
389+ elif specifier == 'c' : # Characters treated as string
390+ return 's'
391+ elif specifier in 'sS' :
392+ return 's'
393+ elif specifier == 'p' :
394+ return '#x'
395+ else :
396+ raise ValueError (f'Unsupported format specifier: { specifier } ' )
397+
398+ def c_format (self , fmt : str , args : Any ) -> str :
399+ """Format a C-style string using Python's format method."""
400+ result_parts = []
401+ i_str = 0
402+ i_arg = 0
403+ while i_str < len (fmt ):
404+ match = self .c_format_regex .search (fmt , i_str )
405+ if not match :
406+ break
407+ py_format = self .convert_to_pythonic_format (match )
408+ formatted_str = self .format (py_format , args [i_arg ] if args else None ) # This will call format_field()
409+ i_arg += 1 if match .group (0 ) != '%%' else 0
410+ result_parts .append (fmt [i_str :match .start ()] + formatted_str )
411+ i_str = match .end ()
412+ # Add remaining part of the string after last match
413+ result_parts .append (fmt [i_str :])
414+ return '' .join (result_parts )
0 commit comments