Skip to content

Commit 3a8cdf8

Browse files
fix(binlog): Fix binlog precision format
1 parent 899b129 commit 3a8cdf8

File tree

7 files changed

+385
-211
lines changed

7 files changed

+385
-211
lines changed

esp_idf_monitor/base/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .binlog import ArgFormatter # noqa: F401

esp_idf_monitor/base/binlog.py

Lines changed: 121 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,14 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
import re
5+
import string
56
import struct
67
from typing import Any, List, Optional, Tuple, Union
78

89
from elftools.elf.elffile import ELFFile
910

1011
from .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

2914
class 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)

test/host_test/inputs/binlog

920 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)