miércoles, 23 de septiembre de 2026

La aventura del compresor de ficheros de Firmadoc de Aytos

0.Introducción

Se me requiere que pase los documentos de Firmadoc a un archivo documental, pero hay documentos guardados en BD que estan comprimidos y no se sabe el formato de compresión.

En las tablas BIB_AY.dbo.DOC y BIB_AY.dbo.DOC_ORI hay un campo llamado DOC_CNT y ORI_CNT respectivamente que contien el binario de un documento comprimido por el algoritmo de Huffman. Pero dicho algoritmo tiene muchas variantes y es parametrizable con lo que no hay manera de saber como descomprimirlo.

Para el resto de ficheros se puede consultar una capeta que Firmadoc tiene en el sistema que está organizada en carpetas diarias. Las carpetas se construen con la siguient función SQL server atacando a la tabla BIB_AY.dbo.IDX, y el nombre de los ficheros junto con la extensión se obtienen

SELECT
   B.IDX_ID as FILE_NAME, B.IDX_EXT as EXTENSION,
  10000*YEAR(B.IDX_FCH)+100*MONTH(B.IDX_FCH)+DAY(B.IDX_FCH) as FOLDER
FROM BIB_AY.dbo.IDX B;


1. Ficheros involucrados

Tras preguntar a la Claude AI y pasarle los ejecutables y DLLs de Firmadoc me indica que parece ser que todo el meollo está en el fichero compress.dll

A continuación le paso este fichero a gemini i me dice:

Este archivo es una Librería de Enlace Dinámico (DLL) de 32 bits compilada nativamente en Visual Basic 5.0 / 6.0.
A continuación, te detallo toda la información técnica extraída directamente de su estructura binaria y los pasos exactos para llevar la lógica de la clase clsHuffman a Python.

📑 1. Información General de la DLL
  • Nombre Original del Archivo: compress.dll [1]
  • Nombre del Producto: compress (Versión 1.01) [1]
  • Desarrollador / Compañía: Ramcor [1]
  • Lenguaje y Entorno: Compilado con el motor de ejecución MSVBVM50.DLL (Visual Basic 5.0), lo que significa que utiliza las funciones clásicas de manejo de memoria, cadenas y arrays de Microsoft VB.
  • Ruta de Desarrollo Original (Hardcoded):
    A:\SICALWIN\Desarrollo\Mi_Codigo_Comun\compress\compress.vbp [1]
  • Funciones de Exportación COM de la DLL: Al ser una DLL de Visual Basic, expone las funciones estándar para registrarse en el sistema operativo:
    • DllCanUnloadNow [1]
    • DllGetClassObject [1]
    • DllRegisterServer [1]
    • DllUnregisterServer [1]

🧩 2. Estructura de Componentes Internos
El binario revela que el proyecto está compuesto por los siguientes módulos y clases autodeclarados:
  1. Module1: Módulo estándar (suele contener el punto de entrada Sub Main).
  2. clsHuffman: Clase principal que expone los algoritmos de compresión y descompresión basados en el algoritmo de Huffman.
Métodos expuestos en la clase clsHuffman:
Examinando las tablas de símbolos y cadenas internas del binario, la clase cuenta con los siguientes métodos públicos (funciones) y parámetros asociados:
  • EncodeFile(SourceFile, DestFile): Comprime un archivo físico y genera un archivo de salida.
  • DecodeFile(SourceFile, DestFile): Descomprime un archivo comprimido previamente en un nuevo destino.
  • EncodeByte(ByteArray, ByteLen): Comprime datos directamente desde un array de bytes en memoria.
  • DecodeByte(ByteArray, ByteLen): Descomprime datos hacia un array de bytes en memoria.
  • EncodeString(Text): Comprime una cadena de texto.
  • DecodeString(Text): Descomprime una cadena de texto.
  • Progress(Procent): Un evento o propiedad interna utilizada para reportar el porcentaje de avance del proceso (0 a 100%).
Firmas del Formato y Errores identificados:
El descompilador de strings revela cómo opera el formato interno de compresión de esta DLL específica:
  • Firma de cabecera: Los archivos comprimidos por esta DLL inician con la firma mágica HE3.
  • Mensaje de error 1: "The data either was not compressed with HE3 or is corrupt (identification string not found)". Esto confirma que al descompilar, el código busca primero estos 3 bytes iniciales.
  • Mensaje de error 2: "The data might be corrupted (checksum did not match expected value)". Confirma que el algoritmo implementa una validación de integridad mediante suma de verificación (checksum) al final o en la cabecera del archivo comprimido.


2. Utilizar la lógica para comprimir y descomprimir ficheros

Y a la vista de esto, vuelvo a consultar a Claude AI y me dice que:
  1. Consiga una máquina Windows por ejm plo Windows 11
  2. Que registre las librerias compress.dll y msvbvm50.dll con l"cmd" en modo administrador:

    C:\Windows\SysWOW64\regsvr32.exe /s "C:\Ruta\compress.dll"
    C:\Windows\SysWOW64\regsvr32.exe /s "C:\Ruta\msvbvm50.dll
    "

  3. Descargar los campos blob (DOC_CNT y ORI_CNT ) de sus tablas correspondientes en ficheros individuales mediante un script de Python dentro de una carpeta
  4. Mediante un script ".vbs" descomprimir los ficheros conservando las extensiones

 
3. Script de python para descargar los ficheros


Para ello hay que atacar a SQL SERVER

y genera los ficheros en la carpeta indicada y le añade la extension que tendria que tener al nombre. Comprobar al final del script (en la función main) usuario, contraseña y url y otros datos de la BD y la carpeta donde descargar los ficheros. Nota este script es para linux. Seguramente se tendrá que cambiar alguno de los parámetros que estan remarcados en fondo amarillo

from sqlalchemy import create_engine, text
from urllib.parse import quote_plus


# --------------------------------------------------------------------------
# Utilidades de configuración
# --------------------------------------------------------------------------
def _leer_conf(conf: dict) -> dict:
    """
    Normaliza el diccionario de configuración aceptando nombres de clave
    alternativos (dbType/type, dbHost/host, etc.). Lanza KeyError claro si
    falta algo obligatorio.
    """
    campos = {
        "dbtype": None, "host": None, "database": None,
        "user": None, "pwd": None, "port": None,
        "driver": None, "schema": None, "trust_cert": None,
    }
    alias = {
        "dbtype": ("dbtype", "type"),
        "host": ("host", "dbhost"),
        "database": ("database", "dbname"),
        "user": ("user", "dbuser"),
        "pwd": ("password", "dbpwd"),
        "port": ("port", "dbport"),
        "driver": ("driver", "dbdriver"),
        "schema": ("schema", "dbschema"),
        "trust_cert": ("trustservercertificate", "dbtrustservercertificate"),
    }
    conf_lower = {k.lower(): v for k, v in conf.items()}
    for campo, nombres in alias.items():
        for nombre in nombres:
            if nombre in conf_lower:
                campos[campo] = conf_lower[nombre]
                break

    obligatorios = ("dbtype", "host", "database", "user", "pwd")
    faltantes = [c for c in obligatorios if not campos[c]]
    if faltantes:
        raise KeyError(f"Faltan claves obligatorias en la configuración: {faltantes}")

    return campos


def _connection_string(conf: dict) -> str:
    c = _leer_conf(conf)
    usuario = quote_plus(str(c["user"]))
    password = quote_plus(str(c["pwd"]))
    host = f"{c['host']}:{c['port']}" if c["port"] else c["host"]

    url = f"{c['dbtype']}://{usuario}:{password}@{host}/{c['database']}"

    params = {}
    if c["driver"]:
        params["driver"] = c["driver"]
    if c["trust_cert"]:
        params["TrustServerCertificate"] = c["trust_cert"]
    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}.bin"
                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}"
                
                with open(nombre_archivo, "wb") as f:
                    f.write(contenido)
                
                archivos_creados += 1
                i+=1
                
            return archivos_creados
            
    finally:
        engine_ms.dispose()


if __name__ == "__main__":
    conf_ms = {
        "dbType": "mssql+pyodbc",
        "dbUser": "myuser",
        "dbPwd": "mypwd",
        "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/myuser/kk/DOCUMENTS_AYTOS",
    )        
    

4. Script vbs para descomprimir los ficheros y asignarles su extensión original en Windows 11

Ahora a¡hay que retocar el scrip para que lea de la carpeta correspondiente los ficheros terminados en ".bin". 
Ojo la Verificar la carpeta de origen que debe ser la misma que la del script anterior. El problema es que antes utilice Linux y ahora Windows 11. Por tanto e3s importante que ambos scripts apunten a la miosma carpeta

Option Explicit

'==================== AJUSTA ESTAS RUTAS ====================
Dim carpeta_origen, carpeta_destino
Dim sufijo_origen, sufijo_destino

carpeta_origen  = "Z:\Compatida|DOCUMENTS_AYTOS\"
carpeta_destino = "Z:\Compatida|DOCUMENTS_AYTOS\"
sufijo_origen = ".bin" ' terminación que buscamos en los ficheros de entrada sufijo_destino = ".bin.desc" ' extensión con la que se guarda ya descomprimido '============================================================== Dim huff, fso Set fso = CreateObject("Scripting.FileSystemObject") ' Comprobar que la carpeta de origen existe If Not fso.FolderExists(carpeta_origen) Then WScript.Echo "ERROR: no existe la carpeta de origen:" & vbCrLf & carpeta_origen WScript.Quit 1 End If ' Crear la carpeta de destino si no existe If Not fso.FolderExists(carpeta_destino) Then fso.CreateFolder(carpeta_destino) End If ' Crear el objeto de la DLL una sola vez para toda la carpeta On Error Resume Next Set huff = CreateObject("Compress.clsHuffman") If Err.Number <> 0 Then WScript.Echo "ERROR al crear el objeto Compress.clsHuffman: " & Err.Description WScript.Quit 1 End If On Error Goto 0 Dim carpeta, fichero, nombre, nombreBase, file_ent, file_sal, fragmentos, extension Dim total, ok, fallos total = 0 ok = 0 fallos = 0 Set carpeta = fso.GetFolder(carpeta_origen) For Each fichero In carpeta.Files nombre = fichero.Name ' ¿Termina el nombre en el sufijo que buscamos? (sin distinguir mayúsculas/minúsculas) If Len(nombre) >= Len(sufijo_origen) Then If LCase(Right(nombre, Len(sufijo_origen))) = LCase(sufijo_origen) Then total = total + 1 nombreBase = Left(nombre, Len(nombre) - Len(sufijo_origen)) WScript.Echo nombreBase 'Edu fragmentos = Split(nombreBase, ".") extension ="unkown" If UBound(fragmentos) >= 1 Then extension = fragmentos(1)     End If 'Fi edu file_ent = fichero.Path file_sal = carpeta_destino & nombreBase & sufijo_destino & "." & extension 'WScript.Echo file_sal 'WScript.Quit On Error Resume Next Err.Clear huff.DecodeFile CStr(file_ent), CStr(file_sal) If Err.Number <> 0 Then WScript.Echo "[ERROR] " & nombre & " -> " & Err.Description & " (0x" & Hex(Err.Number) & ")" fallos = fallos + 1 Else If fso.FileExists(file_sal) Then 'WScript.Echo "[OK] " & nombre & " -> " & file_sal ok = ok + 1 resto = ok Mod 100 If resto = 0 Then WScript.Echo "[OK] " & CStr(ok) & " -> " & file_sal End If Else WScript.Echo "[AVISO] " & nombre & " -> DecodeFile no dio error, pero no se generó la salida" fallos = fallos + 1 End If End If On Error Goto 0 End If End If Next WScript.Echo "" WScript.Echo "Hecho. " & total & " ficheros encontrados, " & ok & " correctos, " & fallos & " con error." Set huff = Nothing Set fso = Nothing


Y con esto ya se pueden obtener los ficheros originales,