Spaces:
Running
Running
import panel as pn | |
import lz4.block | |
import xml.sax.saxutils | |
import html | |
import io | |
import xml.dom.minidom | |
import blackboxprotobuf | |
# from wechatlog.adaptors.pywxdumpAdapter import decode_bytes_extra | |
# Initialize Panel with Bootstrap design | |
pn.extension(design="bootstrap", sizing_mode="stretch_width") | |
# Social media links and icons | |
ICON_URLS = { | |
"brand-github": "https://github.com/yourusername/compressed-content-viewer", | |
"message-circle": "https://discourse.holoviz.org/", | |
} | |
def decode_bytes_extra(data): | |
""" | |
Decode BytesExtra data using pywxdump's decode_bytes_extra function. | |
""" | |
def get_BytesExtra(BytesExtra): | |
BytesExtra_message_type = { | |
"1": { | |
"type": "message", | |
"message_typedef": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "int", | |
"name": "" | |
} | |
}, | |
"name": "1" | |
}, | |
"3": { | |
"type": "message", | |
"message_typedef": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "str", | |
"name": "" | |
} | |
}, | |
"name": "3", | |
"alt_typedefs": { | |
"1": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": {}, | |
"name": "" | |
} | |
}, | |
"2": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"13": { | |
"type": "fixed32", | |
"name": "" | |
}, | |
"12": { | |
"type": "fixed32", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"3": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"15": { | |
"type": "fixed64", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"4": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"15": { | |
"type": "int", | |
"name": "" | |
}, | |
"14": { | |
"type": "fixed32", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"5": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"12": { | |
"type": "fixed32", | |
"name": "" | |
}, | |
"7": { | |
"type": "fixed64", | |
"name": "" | |
}, | |
"6": { | |
"type": "fixed64", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"6": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"7": { | |
"type": "fixed64", | |
"name": "" | |
}, | |
"6": { | |
"type": "fixed32", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"7": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"12": { | |
"type": "fixed64", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"8": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"6": { | |
"type": "fixed64", | |
"name": "" | |
}, | |
"12": { | |
"type": "fixed32", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"9": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"15": { | |
"type": "int", | |
"name": "" | |
}, | |
"12": { | |
"type": "fixed64", | |
"name": "" | |
}, | |
"6": { | |
"type": "int", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
"10": { | |
"1": { | |
"type": "int", | |
"name": "" | |
}, | |
"2": { | |
"type": "message", | |
"message_typedef": { | |
"6": { | |
"type": "fixed32", | |
"name": "" | |
}, | |
"12": { | |
"type": "fixed64", | |
"name": "" | |
} | |
}, | |
"name": "" | |
} | |
}, | |
} | |
} | |
} | |
if BytesExtra is None or not isinstance(BytesExtra, bytes): | |
return None | |
try: | |
deserialize_data, message_type = blackboxprotobuf.decode_message(BytesExtra, BytesExtra_message_type) | |
return deserialize_data | |
except Exception as e: | |
return None | |
return get_BytesExtra(data) | |
def process_content(data): | |
""" | |
Process both CompressContent and BytesExtra content. | |
First tries to decompress (for CompressContent), if fails treats as BytesExtra. | |
:param data: bytes data to process | |
:return: tuple (decoded_string, content_type, error_message) | |
""" | |
if data is None or not isinstance(data, bytes): | |
return "", "unknown", "Invalid input: data must be bytes" | |
# First try to decompress (assuming it's CompressContent) | |
try: | |
dst = lz4.block.decompress(data, uncompressed_size=len(data) << 10) | |
decoded_string = dst.decode().replace("\x00", "") # Remove any null characters | |
return decoded_string, "CompressContent", None | |
except Exception: # If decompression fails, try BytesExtra | |
try: | |
# Try to decode as BytesExtra | |
decoded_dict = decode_bytes_extra(data) | |
if isinstance(decoded_dict, dict): | |
# Convert dictionary to a formatted string representation | |
decoded_string = "{\n" | |
for key, value in decoded_dict.items(): | |
decoded_string += f" {key}: {value}\n" | |
decoded_string += "}" | |
return decoded_string, "BytesExtra", None | |
else: | |
return "", "unknown", "BytesExtra decoding did not return a dictionary" | |
except Exception as decode_error: | |
return "", "unknown", f"Failed to process content: {str(decode_error)}" | |
def unescape_xml(text): | |
# First handle standard XML entities | |
text = xml.sax.saxutils.unescape(text) | |
# Then handle HTML entities that might be present | |
text = html.unescape(text) | |
return text | |
def format_xml(xml_string): | |
try: | |
# Parse the XML string | |
dom = xml.dom.minidom.parseString(xml_string) | |
# Get the pretty-printed XML | |
return dom.toprettyxml(indent=" ") | |
except Exception as e: | |
# If parsing fails, return the original string with the error | |
return f"XML formatting error: {str(e)}\n\nOriginal XML:\n{xml_string}" | |
# Create the UI with modern styling | |
file_input = pn.widgets.FileInput( | |
accept='.bin', | |
height=50, | |
sizing_mode="stretch_width" | |
) | |
content_type_indicator = pn.widgets.StaticText( | |
value="", | |
align="center" | |
) | |
status = pn.widgets.StaticText( | |
value="##### π Upload a .bin file to view its content", | |
align="center" | |
) | |
# Use a TextAreaInput with modern styling | |
result_area = pn.widgets.TextAreaInput( | |
value="", | |
height=400, | |
sizing_mode="stretch_width", | |
disabled=True, | |
stylesheets=['body { font-family: monospace; }'] | |
) | |
# Styled checkbox | |
unescape_checkbox = pn.widgets.Checkbox( | |
name="Unescape XML entities", | |
value=True, | |
align="center", | |
margin=(10, 10) | |
) | |
def get_xml_content(): | |
if processed_xml: | |
return io.StringIO(processed_xml) | |
return None | |
# Modern styled download button | |
download_button = pn.widgets.FileDownload( | |
callback=get_xml_content, | |
filename="content.xml", | |
button_type="primary", | |
label="π₯ Download XML", | |
disabled=True, | |
align="center", | |
height=35 | |
) | |
# Global variable to store the processed XML | |
processed_xml = None | |
def process_file(event): | |
global processed_xml | |
if event.new is not None: | |
status.value = "##### βοΈ Processing file..." | |
content_type_indicator.value = "" | |
try: | |
# Process the content (either CompressContent or BytesExtra) | |
content, content_type, error = process_content(event.new) | |
if error: | |
status.value = f"##### β {error}" | |
result_area.value = "" | |
content_type_indicator.value = "" | |
download_button.disabled = True | |
processed_xml = None | |
return | |
# Update content type indicator with icon | |
icon = "ποΈ" if content_type == "CompressContent" else "π" | |
content_type_indicator.value = f"##### {icon} Detected type: {content_type}" | |
if content: | |
if content_type == "CompressContent": | |
try: | |
# Unescape XML entities if checkbox is checked | |
if unescape_checkbox.value: | |
content = unescape_xml(content) | |
# Format XML with proper indentation | |
pretty_content = format_xml(content) | |
result_area.value = pretty_content | |
processed_xml = pretty_content | |
download_button.filename = f"{file_input.filename.rsplit('.', 1)[0]}_xml.xml" | |
except Exception as xml_error: | |
result_area.value = content | |
processed_xml = content | |
status.value = f"##### β οΈ Content extracted, but XML formatting failed: {str(xml_error)}" | |
else: # BytesExtra | |
result_area.value = content | |
processed_xml = content | |
download_button.filename = f"{file_input.filename.rsplit('.', 1)[0]}_dict.txt" | |
status.value = "##### β Content processed successfully!" | |
download_button.disabled = False | |
else: | |
status.value = "##### β Processing failed" | |
result_area.value = "" | |
download_button.disabled = True | |
processed_xml = None | |
except Exception as e: | |
status.value = f"##### β Error: {str(e)}" | |
result_area.value = "" | |
content_type_indicator.value = "" | |
download_button.disabled = True | |
processed_xml = None | |
file_input.param.watch(process_file, 'value') | |
def toggle_unescape(event): | |
if file_input.value is not None: | |
process_file(pn.param.ParamEvent(obj=file_input, name='value', old=None, new=file_input.value)) | |
unescape_checkbox.param.watch(toggle_unescape, 'value') | |
# Add footer with social links | |
footer_row = pn.Row(pn.Spacer(), align="center") | |
for icon, url in ICON_URLS.items(): | |
href_button = pn.widgets.Button(icon=icon, width=35, height=35) | |
href_button.js_on_click(code=f"window.open('{url}')") | |
footer_row.append(href_button) | |
footer_row.append(pn.Spacer()) | |
# Create main content | |
main = pn.WidgetBox( | |
pn.Column( | |
pn.pane.Markdown("##### π Drop your .bin file here or click to upload"), | |
file_input, | |
content_type_indicator, | |
pn.Row( | |
unescape_checkbox, | |
download_button, | |
align="center" | |
), | |
status, | |
pn.pane.Markdown("##### π Content Preview"), | |
result_area, | |
footer_row, | |
sizing_mode="stretch_width" | |
) | |
) | |
# Create the template | |
title = "Content Viewer" | |
template = pn.template.BootstrapTemplate( | |
title=title, | |
main=main, | |
main_max_width="min(80%, 1000px)", | |
header_background="#F08080", | |
) | |
template.servable(title=title) | |