0. Introducción
Esto parece una película. Primeramente busqué en Google "Algoritmo de Huffman en Visual Basic" y me hace referencia a esta URL de jrubi que ha encontrado el algoritmo de Huffman realizado por James Vincent Carnicelli en agosto del 2000, y le pasé el código a Claude para que me lo convirtiera a Python.
Como Claude tenía ya muestras de ficheros originales y comprimidos, me contesta que el algoritmo casi daba resultados correctos pero que era una versión antigua, en vez de darle cabecera "HF3" daba "HF2", y me propone buscar el algoritmo más reciente en internet y le digo que si.
Entonces encuentra esta URL de post.bytes que dice que es :"Es un hilo de 2005 (espejo de un antiguo grupo de noticias de Microsoft sobre VB.NET) donde un usuario llamado "Crouchie1998" responde a una pregunta pegando el código VB6 completo de la clase clsHuffman, atribuido originalmente a David Midkiff. Ese mismo mensaje también enlaza a dos copias del programa VB6 ya compilado (Planet Source Code y a1vbcode.com), aunque esos enlaces concretos ya no funcionan hoy en día — el texto del código en sí es lo que pude usar."
Y primeramente prueba el algoritmo en VB.NET sobre los ficheros que tenía y me suelta "BINGO!" y me genera el algoritmo en Python completamente funcional.
Con esto conseguimos no utilizar código que puede ser propiedad intelectual de Aytos como compress.dll (aunque el algoritmo subyacente no sea de su propiedad) y poder trabajar perfectamente en Linux para obtener estos ficheros y tener un histórico documental
1. Algoritmo de Huffman de compresión y decompresión
- En la función main_edu() hay que darle la relació de ficheros a descomprimir
- Los nombres de ficheros serán del tipo "12345.EXT.bin" donde "12345" representa el campo IDX_ID de la tabla BIB_AY.dbo.IDX que es numérico; "EXT" represnta la extensión por ejemplo "DOC", "PDF" o la que sea, y "bin" es constante y significa que está comprido. Ejemplos son "345123.PDF.bin", "24512.DOC.bin"
- Al final genera el fichero con el mismo nombre al que se le ha añadido la extensiónb correspondiente, por ejemplo en los dos casos anteriores generaría "345123.PDF.bin.PDF", "24512.DOC.bin.DOC"
#!/usr/bin/env python3 """ decode_he3.py -------------- Codificador/decodificador puro-Python del formato "HE3" usado por compress.dll (clase clsHuffman, de David Midkiff). Reimplementa exactamente el algoritmo Huffman original (encontrado en su código fuente VB6), sin depender de Windows, Wine ni la DLL original. Verificado byte a byte contra ficheros reales: tanto DecodeFile como EncodeFile producen un resultado IDÉNTICO al de la DLL original (incluido el checksum interno del propio formato). USO: Descomprimir un fichero: python3 decode_he3.py decode origen.bin destino.pdf Comprimir un fichero: python3 decode_he3.py encode origen.pdf destino.bin Toda una carpeta (descomprimir todos los *.bin): python3 decode_he3.py decode --batch carpeta_origen carpeta_destino Toda una carpeta (comprimir): python3 decode_he3.py encode --batch carpeta_origen carpeta_destino --ext .pdf --suffix .aytos FORMATO DEL FICHERO (para referencia): offset 0-3 : magia "HE3" + CR (0x48 0x45 0x33 0x0D) (si el 3er byte es '0'=0x30, el fichero no se llegó a comprimir y el resto son los datos originales tal cual) offset 4 : checksum (XOR de todos los bytes originales) offset 5-8 : Long (LE32) = tamaño del fichero original offset 9-10 : Integer (LE16) = número de símbolos (valores de byte) distintos usados en el fichero luego, por cada símbolo (en orden ascendente de valor de byte): 1 byte = valor del símbolo (0-255) 1 byte = longitud en bits de su código Huffman luego, en el mismo orden ascendente de símbolo: los propios patrones de bits de cada código, empaquetados LSB-first, concatenados y sin relleno entre símbolos (solo relleno al final, hasta completar el byte) a partir de ahí, alineado a byte: los datos reales comprimidos, empaquetados también LSB-first, decodificables recorriendo el árbol binario reconstruido (bit 0 = rama izquierda, bit 1 = rama derecha) hasta producir tantos bytes como indica el tamaño original. """ import argparse import os import struct import sys class HE3Error(Exception): pass class _BitReader: """Lee bits LSB-first de un buffer, empezando en un byte concreto.""" __slots__ = ('data', 'pos', 'bitpos') def __init__(self, data: bytes, start_byte: int = 0): self.data = data self.pos = start_byte self.bitpos = 0 def read_bit(self) -> int: byte = self.data[self.pos] bit = (byte >> self.bitpos) & 1 self.bitpos += 1 if self.bitpos == 8: self.bitpos = 0 self.pos += 1 return bit def align_byte(self) -> None: if self.bitpos != 0: self.bitpos = 0 self.pos += 1 class _Node: __slots__ = ('left', 'right', 'value') def __init__(self): self.left = None self.right = None self.value = -1 def decode_he3(data: bytes) -> bytes: """Decodifica el contenido binario (formato HE3) y devuelve los bytes originales. Lanza HE3Error si la cabecera o el checksum no son validos.""" if len(data) < 4 or data[0] != 72 or data[1] != 69 or data[3] != 13: raise HE3Error("Cabecera invalida: no es un fichero HE0/HE3") if data[2] == 48: # '0' -> el fichero se guardo sin comprimir return data[4:] if data[2] != 51: # '3' raise HE3Error(f"Version de formato no soportada (byte2={data[2]})") checksum = data[4] orig_size = struct.unpack('<I', data[5:9])[0] if orig_size == 0: return b'' count = struct.unpack('<H', data[9:11])[0] pos = 11 symbols = [] lengths = {} for _ in range(count): sym = data[pos]; pos += 1 length = data[pos]; pos += 1 symbols.append(sym) lengths[sym] = length # Leer los patrones de bits de cada codigo (orden raiz->hoja) br = _BitReader(data, pos) codes = {} for sym in symbols: bits = [br.read_bit() for _ in range(lengths[sym])] codes[sym] = bits br.align_byte() # Reconstruir el arbol binario: 0 = izquierda, 1 = derecha root = _Node() for sym, bits in codes.items(): node = root for b in bits: if b == 0: if node.left is None: node.left = _Node() node = node.left else: if node.right is None: node.right = _Node() node = node.right node.value = sym # Decodificar el resto del fichero recorriendo el arbol out = bytearray() node = root for byte in data[br.pos:]: for bitpos in range(8): bit = (byte >> bitpos) & 1 node = node.right if bit else node.left if node is None: raise HE3Error("Arbol Huffman corrupto durante la decodificacion") if node.value > -1: out.append(node.value) if len(out) == orig_size: chk = 0 for b in out: chk ^= b if chk != checksum: raise HE3Error( f"Checksum no coincide (esperado {checksum}, " f"calculado {chk}) - el fichero podria estar corrupto" ) return bytes(out) node = root raise HE3Error("Fin del fichero antes de completar la decodificacion") def encode_he3(data: bytes) -> bytes: """Comprime bytes al formato HE3. Devuelve el mismo resultado, byte a byte, que produciria clsHuffman.EncodeFile().""" byte_len = len(data) if byte_len == 0: return bytes([72, 69, 48, 13]) # "HE0" + CR, fichero vacio counts = [0] * 256 for b in data: counts[b] += 1 class _ENode: __slots__ = ('weight', 'left', 'right', 'parent', 'value') def __init__(self, weight, value=-1, left=-1, right=-1): self.weight = weight self.value = value self.left = left self.right = right self.parent = -1 nodes = [_ENode(counts[i], value=i) for i in range(256) if counts[i] > 0] nodes_count = len(nodes) nodes.extend([None] * (nodes_count - 1)) # espacio para nodos internos n = nodes_count while n > 1: lnode1 = lnode2 = -1 lweight1 = lweight2 = 0 # Replica exacta del algoritmo O(n^2) de "dos mas pequenos" del # codigo VB original, incluido su criterio de desempate. for i, node in enumerate(nodes): if node is None or node.parent != -1: continue if lnode1 == -1: lweight1 = node.weight; lnode1 = i elif lnode2 == -1: lweight2 = node.weight; lnode2 = i elif node.weight < lweight1: if node.weight < lweight2: if lweight1 < lweight2: lweight2 = node.weight; lnode2 = i else: lweight1 = node.weight; lnode1 = i else: lweight1 = node.weight; lnode1 = i elif node.weight < lweight2: lweight2 = node.weight; lnode2 = i new_index = next(i for i, node in enumerate(nodes) if node is None) nodes[new_index] = _ENode(lweight1 + lweight2, left=lnode1, right=lnode2) nodes[lnode1].parent = new_index nodes[lnode2].parent = new_index n -= 1 root_index = next(i for i, node in enumerate(nodes) if node is not None and node.parent == -1) codes = {} def walk(idx, bits): node = nodes[idx] if node.value > -1: codes[node.value] = bits return if node.left != -1: walk(node.left, bits + [0]) if node.right != -1: walk(node.right, bits + [1]) walk(root_index, []) used_symbols = sorted(codes.keys()) total_bits = sum(len(codes[i]) * counts[i] for i in used_symbols) l_length = (total_bits + 7) // 8 if l_length == 0 or l_length > byte_len: return bytes([72, 69, 48, 13]) + data # fallback: sin comprimir checksum = 0 for b in data: checksum ^= b out = bytearray() out += bytes([72, 69, 51, 13]) out.append(checksum) out += struct.pack('<I', byte_len) out += struct.pack('<H', len(used_symbols)) for sym in used_symbols: out.append(sym) out.append(len(codes[sym])) bitbuf = 0 bitcount = 0 def push_bit(bit): nonlocal bitbuf, bitcount if bit: bitbuf |= (1 << bitcount) bitcount += 1 if bitcount == 8: out.append(bitbuf) bitbuf = 0 bitcount = 0 for sym in used_symbols: for bit in codes[sym]: push_bit(bit) if bitcount > 0: out.append(bitbuf) bitbuf = 0 bitcount = 0 for b in data: for bit in codes[b]: push_bit(bit) if bitcount > 0: out.append(bitbuf) return bytes(out) def process_file(src: str, dst: str, mode: str) -> None: with open(src, 'rb') as f: data = f.read() result = decode_he3(data) if mode == 'decode' else encode_he3(data) out_dir = os.path.dirname(os.path.abspath(dst)) if out_dir: os.makedirs(out_dir, exist_ok=True) with open(dst, 'wb') as f: f.write(result) def main_edu() -> None: files=[ "/home/user/kk/DOCUMENTS_AYTOS/238476.PDF.bin", "/home/user/kk/DOCUMENTS_AYTOS/233831.DOC.bin", ] fail=0 ok=0 for fname in files: parts=fname.split(".") f_out=fname + "." + parts[1] try: process_file(fname, f_out, "decode") print(f"[OK] {fname} -> {os.path.basename(f_out)}") ok += 1 except Exception as exc: print(f"[ERROR] {fname}: {exc}") fail += 1 print(f"\nHecho. {ok} correctos, {fail} con error.") if __name__ == "__main__": main_edu()
2. Descargar los ficheros desde BD a disco
Aquí ya no escribimos el fichero encriptado/comprimido a disco sinó que escribimos el fichero drirectamente a disco con su nombre y extensión correspondiente.
En mi caso he guardado el fichero anterior en la misma carpeta y lo he llamado xmhuffman.py Por tanto la tercera línea se debe de ajustar al nombre que se le haya dado a dicho fichero. En la función main hay que darle los parámetros de acceso a la BD y también la rudta donde guardar los ficheros.
from sqlalchemy import create_engine, text from urllib.parse import quote_plus from xmhuffman import decode_he3 # -------------------------------------------------------------------------- # Utilidades de configuración # -------------------------------------------------------------------------- def _connection_string(c: dict) -> str: usuario = quote_plus(str(c["dbUser"])) password = quote_plus(str(c["dbPwd"])) host = f"{c['dbHost']}:{c['dbPort']}" if c["dbPort"] else c["dbHost"] url = f"{c['dbType']}://{usuario}:{password}@{host}/{c['dbName']}" params = {} if c["dbDriver"]: params["driver"] = c["dbDriver"] if c["dbTrustServerCertificate"]: params["TrustServerCertificate"] = c["dbTrustServerCertificate"] if params: url += "?" + "&".join(f"{k}={quote_plus(str(v))}" for k, v in params.items()) return url def _create_sync_engine(conf: dict): """Engine síncrono, para SQL Server (pyodbc).""" return create_engine(_connection_string(conf)) def copy_all_blobs2files( query_ms: str, conf_ms: dict, ruta_salida: str, ): engine_ms = _create_sync_engine(conf_ms) archivos_creados = 0 try: with engine_ms.connect() as conn: # Usamos fetchall() para traer todos los registros de la consulta resultados = conn.execute(text(query_ms)).fetchall() if not resultados: raise ValueError(f"No se encontraron filas para la consulta: {query_ms}") i=0 for fila in resultados: if i==2000: break # fila[0] debe ser el ID (o nombre del archivo) y fila[1] el BLOB id_registro = fila[0] extension = fila[1] contenido = fila[2] nombre_archivo=f"{id_registro}.{extension}" print(nombre_archivo) # Omitimos registros si el BLOB está vacío if contenido is None: print(f"Advertencia: El campo BLOB está vacío para el ID {id_registro}. Saltando...") continue # Construimos la ruta completa del archivo nombre_archivo = f"{ruta_salida}{nombre_archivo}" result = decode_he3(contenido) with open(nombre_archivo, 'wb') as f: f.write(result) archivos_creados += 1 i+=1 return archivos_creados finally: engine_ms.dispose() if __name__ == "__main__": conf_ms = { "dbType": "mssql+pyodbc", "dbUser": "myuser", "dbPwd": "mypassword", "dbHost": "192.168.xxx.xxx", "dbPort": 1433, "dbName": "BIB_AY", "dbDriver": "ODBC Driver 18 for SQL Server", "dbTrustServerCertificate": "yes", } query_ms_all = """ SELECT B.IDX_ID, B.IDX_EXT, ISNULL(d1.DOC_CNT,d2.ORI_CNT) as doc_blob,B.IDX_ALI, d1.DOC_TAM, d2.ORI_TAM, 10000*YEAR(B.IDX_FCH)+100*MONTH(B.IDX_FCH)+DAY(B.IDX_FCH) as FOLDER FROM BIB_AY.dbo.IDX B LEFT JOIN BIB_AY.dbo.DOC d1 ON d1.DOC_ID=B.IDX_ID LEFT JOIN BIB_AY.dbo.DOC_ORI d2 ON d2.ORI_ID=B.IDX_ID WHERE d1.DOC_TAM>0 or d2.ORI_TAM>0; """ resultado = copy_all_blobs2files( query_ms=query_ms_all, conf_ms=conf_ms, ruta_salida="/home/user/kk/DOCUMENTS_AYTOS/", )