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)