Repository navigation
Formato del paquete de mazo
English · Español
Un fichero .pbdeck es un ZIP que lleva un mazo junto con todas las imágenes que usa, para poder moverlo a otro ordenador (o hacer una copia de seguridad) sin perder nada. Código: src-tauri/src/package.rs (export, import, blobs_del_deck), comandos deck_export_package / deck_import_package en src-tauri/src/commands/library.rs.
El formato es deliberadamente sencillo: un manifiesto JSON más los ficheros de imagen originales con el nombre de su SHA-256. La importación desconfía de todo lo que hay en el fichero.
x.pbdeck
├── manifest.json (Deflate-compressed)
└── blobs/
├── <sha256>.jpg|png|webp (stored, not recompressed)
└── ... (one entry per distinct image)
Se serializa desde la struct privada Manifiesto (JSON con sangrado):
| Campo | Tipo | Significado |
|---|---|---|
format |
string | Debe ser igual a FORMATO = "proxy-builder-deck". |
version |
u32 | Versión del paquete, actualmente VERSION = 2. |
deck |
Deck |
El valor completo de Modelo del mazo, como JSON (cartas, ajustes, reverso del mazo, decklistText, marcas, diseños...). |
colecciones |
Vec<Collection> |
Las colecciones del diseñador a las que hacen referencia los diseños del mazo (design.collection). #[serde(default)]. |
biblioteca |
opcional | Datos de la biblioteca: etiquetas ([{nombre, color, icono?}]; icono, un id de Iconify, solo se escribe si la etiqueta tiene uno), notas (string), idioma (anulación de idioma opcional), listas (las listas para jugar de la mesa de juego, [ListaDeJuego] con ids de copias de este deck; no se escribe si está vacío). #[serde(default)]; cada campo interior también tiene valor por defecto. |
Ejemplo breve de la forma (abreviado):
{ "format": "proxy-builder-deck", "version": 2,
"deck": { "id": "...", "name": "...", "cards": [ ... ], "settings": { ... } },
"colecciones": [],
"biblioteca": { "etiquetas": [{ "nombre": "Tournament", "color": "red" }],
"notas": "", "idioma": "es" } }blobs_del_deck(deck) devuelve el conjunto (un BTreeSet, de modo que el orden es estable) de hashes de: el reverso del mazo, cada anverso de carta, cada reverso Dfc/Custom y cada imagen del diseñador (CardDesign::blobs(): ilustración, blobs de origen del «recorte», emblema propio, anverso y reverso «antes»). escribir añade el emblema de cada colección exportada. Cada uno se escribe una sola vez como blobs/<hash>.<ext>, donde ext sale del fichero guardado.
| No incluido | Consecuencia en el otro ordenador |
|---|---|
Letras y packs de marcos importados (app_data_dir/fuentes, marcos/) |
Los diseños recurren a los incluidos (las letras y los packs «de la casa»). |
| Indicador de favorito, carta de portada elegida | No forman parte de Biblioteca. |
| Ids de carta | Se regeneran al importar. |
| Caché de metadatos de Scryfall | No hace falta: CardMeta está dentro de cada carta. |
version del paquete |
Qué añade |
|---|---|
| 1 | Mazo + imágenes. |
| 2 | Datos del diseñador (DeckCard.design) y las colecciones que usan (colecciones). |
biblioteca (etiquetas, notas y, más tarde, la anulación de idioma y las listas para jugar) se añadió sin subir version: es opcional, y serde ignora los campos desconocidos en las apps antiguas, que simplemente no la ven. El mismo patrón se usa para campos posteriores de Deck (véase Modelo del mazo).
Reglas de importación, en orden (package::import):
- Un ZIP que no se puede leer es
InvalidInput(motivo = zip); un error de E/S esIo. - Sin
manifest.json, o con uno que no se deserializa aManifiesto:InvalidInput,motivo = manifiesto. Ten en cuenta que el análisis ocurre mientras se recorren las entradas, antes de comprobar la versión, así que un paquete de una app futura cuyo JSON dedeckya no encaja con los tipos actuales informa demanifiesto, no de «no admitido». Deducido del orden del código. -
format != "proxy-builder-deck":InvalidInput,motivo = formato. -
version > VERSION:Unsupportedconparams.version. Así es como una app 1.x rechaza un paquete v2 (si no, perdería los diseños en su siguiente guardado). - Un paquete con una versión menor se importa con normalidad; los campos nuevos que faltan toman sus valores serde por defecto.
La version del paquete es independiente de DECK_SCHEMA_VERSION (ahora ambas valen 2 por casualidad). El schema_version del propio mazo dentro del manifiesto no se comprueba al importar; decks::save estampa el valor actual.
flowchart TD
A["menu-exportar.tsx / library-screen.tsx: guardarAhora()"] --> B["guardarParaExportar(): elegir ruta .pbdeck"]
B --> C["deck_export_package(id, path)"]
C --> D["decks::open: mazo desde la base de datos"]
D --> E["package::export"]
E --> F["escribir: manifiesto + colecciones + etiquetas/notas/idioma"]
F --> G["por cada hash en blobs_del_deck + emblemas: BlobStore::locate, leer fichero"]
G --> H["ZIP construido en memoria (Cursor)"]
H --> I["escribir_atomico: .part y luego renombrar"]
I --> J["devuelve el tamaño en bytes a la interfaz"]
Detalles:
- La pantalla primero vuelca las ediciones pendientes (
guardarAhora()), luego pide un destino mediante el ayudante de diálogo compartidoguardarParaExportar(src/lib/dialogos.ts, que recuerda la última carpeta de exportación) y por último llama al comando. Dos puntos de entrada: el menú «...» junto a «Exportar PDF» (src/features/export/menu-exportar.tsx) para el mazo abierto, y la acción por mazo de la pantalla de la biblioteca (src/features/library/library-screen.tsx). - La exportación lee el mazo de la base de datos (
decks::open), no de la pantalla, y por eso el volcado va primero. - El ZIP se construye por completo en memoria y se escribe de forma atómica; un fallo nunca deja un
.pbdeckparcial. Un fichero de blob que falta esNotFound(params.blob). - Salida determinista: las entradas usan una marca de tiempo fija (
zip::DateTime::default()) y los blobs llegan ordenados, de modo que exportar dos veces el mismo mazo da ficheros idénticos byte a byte (testel_mismo_deck_da_el_mismo_fichero). - Las imágenes van
Storedy el manifiestoDeflated: las imágenes ya están comprimidas.
flowchart TD
A["library-screen.tsx: abrirParaImportar(.pbdeck)"] --> B["deck_import_package(path)"]
B --> C["package::import(reader)"]
C --> D["por cada entrada del ZIP: leer_entrada"]
D --> E{"nombre"}
E -- "manifest.json" --> F["interpretar Manifiesto"]
E -- "blobs/xxxx.ext" --> G["validar nombre, el SHA-256 debe coincidir"]
E -- otro --> H["ignorado"]
F --> I["comprobar format y version"]
G --> I
I --> J["¿está en el zip o ya en local toda imagen que necesita el mazo?"]
J --> K["BlobStore::put_bytes por cada imagen (origen tomado del mazo)"]
K --> L["colecciones: añadir las cuyo id no existe en local"]
L --> M["id de mazo nuevo + ids de carta nuevos, renombrar si el nombre existe"]
M --> N["decks::save"]
N --> O["etiquetas, notas, idioma, listas para jugar"]
O --> P["devuelve el Deck nuevo, la biblioteca se recarga"]
Comportamiento paso a paso:
-
Entradas. Cada entrada se lee entera en memoria con
leer_entrada. Los nombres pasan porZipFile::enclosed_name, que rechaza..y las rutas absolutas (zip-slip), y después se normalizan las barras invertidas. Los nombres inseguros fallan conInvalidInput(motivo = ruta). Las entradas distintas demanifest.jsonyblobs/...se ignoran sin avisar. -
Entradas de blob. El nombre debe ser
blobs/<hash>.<ext>con un<hash>válido de 64 caracteres hex y sin barras de más (es_huella); si no,ruta. Se calcula el hash de los bytes y debe ser igual al hash del nombre, o falla toda la importación (motivo = huella,params.blob). No se fía de la extensión del nombre del fichero:BlobStore::put_bytesdetecta el formato real a partir de los bytes. -
Completitud. Todo hash de
blobs_del_deck(&deck)debe estar en el ZIP o existir ya en local (blobs.locate); si no,InvalidInput(motivo = falta_imagen). Esta comprobación no cubre los emblemas de las colecciones; esos los verifica despuéscolecciones::save(blob_missing). -
Imágenes.
put_bytesguarda cada imagen (rechaza lo que no sea imagen conUnsupported/Imagey los lados de más de 12000 px). El origen se busca en el mazo (deck_origen), de modo que una imagen de Scryfall conservaScryfallPng/ScryfallLarge(y por tanto el comportamiento del oscurecimiento de bordes); las desconocidas pasan aUpload. Los blobs existentes se deduplican sin coste. -
Colecciones. Una colección cuyo
idya existe en local no se sustituye: gana la versión de este ordenador. Las demás se guardan concolecciones::save, que además escribe el emblema enblob_refs. -
Identidad del mazo. El mazo importado siempre recibe un uuid nuevo y cada carta uno nuevo (así, importar dos veces el mismo fichero da dos mazos independientes). Si existe un mazo con exactamente el mismo
name, se renombra a"<name> (importado)"(este sufijo en español está fijado enpackage.rs, lo que se aparta de la regla de que Rust nunca escribe texto para el usuario). -
Datos de la biblioteca. Etiquetas: cada nombre se crea o se empareja, sin distinguir mayúsculas, con una etiqueta local existente; una etiqueta existente conserva su color y su icono locales; el icono del paquete solo se le pone a una etiqueta que no tiene. Las notas se aplican si no están vacías. La anulación de idioma se aplica solo si es uno de los códigos admitidos (
etiquetas::IDIOMAS); si no, se ignora. Las listas para jugar se vuelven a guardar con un id nuevo y con sus ids de copias traducidos a los ids nuevos de las cartas (nuevos, el mapa que se hace al renumerarlas); los ids que no están en el deck se quitan (Mesa de juego). -
Resultado. Se devuelve el
Decknuevo y la biblioteca se recarga. El comando abre el fichero conBufReader.
| Amenaza | Defensa |
|---|---|
Zip-slip (../x, /etc/passwd) |
enclosed_name; se rechaza con ruta (test una_ruta_con_puntos_se_rechaza). |
| Imagen manipulada o cambiada | El hash debe coincidir con el nombre (test una_imagen_alterada_se_rechaza). También protege contra ficheros mal etiquetados. |
| Bomba zip en una sola entrada |
MAX_ENTRADA = 512 MiB comprobado contra el tamaño declarado y aplicado durante la lectura (take(MAX_ENTRADA + 1)); error tamano. |
Contenido que no es imagen bajo blobs/
|
probe en put_bytes lo rechaza. |
| Imágenes demasiado grandes | Tope de 12000 px por lado en put_bytes. |
| Tipo de fichero equivocado | Comprobación de format; test sin_manifiesto_o_de_otro_formato_no_entra. |
| Paquete de una app más nueva |
Unsupported. |
| Mazo que referencia una imagen que falta | Se rechaza de entrada (falta_imagen) y de nuevo en decks::save (blob_missing). |
Lagunas conocidas que conviene tener presentes (leídas del código; Deducido donde se indica como riesgo):
- No hay tope al número de entradas ni al tamaño total descomprimido, y cada entrada se almacena en memoria, así que un paquete manipulado con muchísimas entradas grandes puede consumir mucha memoria. El tope de 512 MiB por entrada es el único límite.
- La importación no es transaccional entre pasos. Las imágenes y las colecciones se escriben antes de guardar el mazo, y las etiquetas se aplican después. Si falla un paso posterior, las imágenes anteriores quedan en el almacén de blobs como huérfanas (la recolección de basura las elimina tras el periodo de gracia de 24 h, véase Almacenamiento y blobs) y una colección recién guardada permanece.
- El usuario ve un texto de error genérico, porque la pantalla traduce solo por código de error (
src/lib/errores.ts, por ejemploInvalidInputpasa a «Los datos no son válidos.»); elmotivoconcreto no se muestra. Véase IPC y manejo de errores.
| Test | Qué protege |
|---|---|
ida_y_vuelta_en_un_equipo_limpio |
Ida y vuelta a una biblioteca vacía, ids nuevos, mismo conjunto de blobs, la segunda importación se renombra «(importado)». |
las_etiquetas_y_las_notas_viajan |
Las etiquetas se fusionan por nombre con las existentes, notas e idioma. |
las_listas_para_jugar_viajan_con_los_ids_nuevos |
Una lista para jugar viaja, sigue los ids nuevos de las cartas y pierde un id que no estaba en el deck. |
un_deck_disenado_viaja_con_su_arte_y_su_coleccion |
Los diseños, la ilustración, el emblema y la colección sobreviven, y blob_refs protege la ilustración de la recolección en la máquina nueva. |
el_mismo_deck_da_el_mismo_fichero |
Salida determinista. |
una_imagen_alterada_se_rechaza |
Validación del hash. |
una_ruta_con_puntos_se_rechaza |
Zip-slip. |
sin_manifiesto_o_de_otro_formato_no_entra |
Comprobaciones del manifiesto y del formato. |
- Añadir datos al paquete: hazlos un campo opcional con
#[serde(default)]enManifiestooBiblioteca, rellénalo enescribiry aplícalo al final deimport. SubeVERSIONsolo cuando una app antigua perdería o malinterpretaría datos (como pasó con los diseños); si no, déjala igual para que las apps antiguas sigan importando. - Añadir un lugar nuevo que guarde hashes de imagen: inclúyelo en
blobs_del_deck; si no, el paquete quedará incompleto y la importación fallará confalta_imagen. - Endurecer la importación: añade topes de tamaño total y de número de entradas en el bucle de
import.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia