Repository navigation
Deck Package Format
English · Español
A .pbdeck file is a ZIP that carries one deck together with every image it uses, so it can be moved to another computer (or backed up) without losing anything. Code: src-tauri/src/package.rs (export, import, blobs_del_deck), commands deck_export_package / deck_import_package in src-tauri/src/commands/library.rs.
The format is deliberately boring: a JSON manifest plus the original image files named by their SHA-256. Import distrusts everything in the file.
x.pbdeck
├── manifest.json (Deflate-compressed)
└── blobs/
├── <sha256>.jpg|png|webp (stored, not recompressed)
└── ... (one entry per distinct image)
Serialized from the private Manifiesto struct (pretty-printed JSON):
| Field | Type | Meaning |
|---|---|---|
format |
string | Must equal FORMATO = "proxy-builder-deck". |
version |
u32 | Package version, currently VERSION = 2. |
deck |
Deck |
The whole Deck-Model value, as JSON (cards, settings, default back, decklistText, marks, designs...). |
colecciones |
Vec<Collection> |
The designer collections referenced by the designs in the deck (design.collection). #[serde(default)]. |
biblioteca |
optional | Library data: etiquetas ([{nombre, color}]), notas (string), idioma (optional language override), listas (the game table's game lists, [ListaDeJuego] with copy ids of this deck; omitted when empty). #[serde(default)]; every inner field also defaults. |
Short example of the shape (abridged):
{ "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) returns the set (a BTreeSet, so order is stable) of hashes of: the default back, every card front, every Dfc/Custom back, and every designer image (CardDesign::blobs(): art, "crop" source blobs, own emblem, "before" front and back). escribir adds the emblem of each exported collection. Each is written once as blobs/<hash>.<ext> where ext comes from the stored file.
| Not included | Consequence on the other computer |
|---|---|
Imported fonts and frame packs (app_data_dir/fuentes, marcos/) |
Designs fall back to the built-in ones ("the house" fonts and packs). |
| Favourite flag, chosen cover card | Not part of Biblioteca. |
| Card ids | Regenerated on import. |
| Scryfall metadata cache | Not needed: CardMeta is inside each card. |
Package version
|
What it adds |
|---|---|
| 1 | Deck + images. |
| 2 | Designer data (DeckCard.design) and the collections they use (colecciones). |
biblioteca (tags, notes, and later the language override and the game lists) was added without bumping version: it is optional, and serde ignores unknown fields in older apps, which simply do not see it. The same pattern is used for later Deck fields (see Deck-Model).
Import rules, in order (package::import):
- A ZIP that cannot be read is
InvalidInput(motivo = zip); an IO error isIo. - No
manifest.json, or one that does not deserialize intoManifiesto:InvalidInput,motivo = manifiesto. Note the parse happens while scanning entries, before the version check, so a package from a future app whosedeckJSON no longer fits the current types reportsmanifiesto, not "unsupported". Inferred from the order in the code. -
format != "proxy-builder-deck":InvalidInput,motivo = formato. -
version > VERSION:Unsupportedwithparams.version. This is how a 1.x app rejects a v2 package (it would otherwise lose the designs on its next save). - A package with a lower version imports normally; missing newer fields take their serde defaults.
The package version is independent of DECK_SCHEMA_VERSION (both happen to be 2 now). The deck's own schema_version inside the manifest is not checked on import; decks::save stamps the current value.
flowchart TD
A["menu-exportar.tsx / library-screen.tsx: guardarAhora()"] --> B["guardarParaExportar(): pick .pbdeck path"]
B --> C["deck_export_package(id, path)"]
C --> D["decks::open: deck from the database"]
D --> E["package::export"]
E --> F["escribir: manifest + collections + tags/notes/language"]
F --> G["for each hash in blobs_del_deck + emblems: BlobStore::locate, read file"]
G --> H["ZIP built in memory (Cursor)"]
H --> I["escribir_atomico: .part then rename"]
I --> J["returns size in bytes to the UI"]
Details:
- The screen first flushes pending edits (
guardarAhora()), then asks for a destination through the shared dialog helperguardarParaExportar(src/lib/dialogos.ts, which remembers the last export folder), then calls the command. Two entry points: the "..." menu next to "Export PDF" (src/features/export/menu-exportar.tsx) for the open deck, and the library screen's per-deck action (src/features/library/library-screen.tsx). - The export reads the deck from the database (
decks::open), not from the screen, which is why the flush comes first. - The ZIP is built fully in memory and written atomically; a crash never leaves a partial
.pbdeck. A missing blob file isNotFound(params.blob). - Deterministic output: entries use a fixed timestamp (
zip::DateTime::default()), blobs come in sorted order, so exporting the same deck twice yields byte-identical files (testel_mismo_deck_da_el_mismo_fichero). - Images are
Stored, the manifest isDeflated: images are already compressed.
flowchart TD
A["library-screen.tsx: abrirParaImportar(.pbdeck)"] --> B["deck_import_package(path)"]
B --> C["package::import(reader)"]
C --> D["for each ZIP entry: leer_entrada"]
D --> E{"name"}
E -- "manifest.json" --> F["parse Manifiesto"]
E -- "blobs/xxxx.ext" --> G["validate name, SHA-256 must match"]
E -- other --> H["ignored"]
F --> I["check format and version"]
G --> I
I --> J["every image the deck needs is in the zip or already local?"]
J --> K["BlobStore::put_bytes for each image (origin taken from the deck)"]
K --> L["collections: add those whose id does not exist locally"]
L --> M["new deck id + new card ids, rename if the name exists"]
M --> N["decks::save"]
N --> O["tags, notes, language, game lists"]
O --> P["returns the new Deck, library reloads"]
Step-by-step behaviour:
-
Entries. Each entry is read fully into memory by
leer_entrada. Names pass throughZipFile::enclosed_name, which rejects..and absolute paths (zip-slip), then backslashes are normalised. Unsafe names fail withInvalidInput(motivo = ruta). Entries other thanmanifest.jsonandblobs/...are silently ignored. -
Blob entries. The name must be
blobs/<hash>.<ext>with a valid 64-hex<hash>and no extra slash (es_huella); otherwiseruta. The bytes are hashed and must equal the name's hash or the whole import fails (motivo = huella,params.blob). The extension in the file name is not trusted:BlobStore::put_bytesprobes the real format from the bytes. -
Completeness. Every hash in
blobs_del_deck(&deck)must either be in the ZIP or already exist locally (blobs.locate), elseInvalidInput(motivo = falta_imagen). This check does not cover collection emblems; those are verified later bycolecciones::save(blob_missing). -
Images.
put_bytesstores each image (rejecting non-images withUnsupported/Imageand sides over 12000 px). The origin is looked up in the deck (deck_origen), so a Scryfall image keepsScryfallPng/ScryfallLarge(and therefore edge-darkening behaviour); unknown ones becomeUpload. Existing blobs are deduplicated for free. -
Collections. A collection whose
idalready exists locally is not replaced: this computer's version wins. Others are saved throughcolecciones::save, which also writes the emblem intoblob_refs. -
Deck identity. The imported deck always gets a new uuid and every card a new uuid (so importing the same file twice yields two independent decks). If a deck with exactly the same
nameexists, it is renamed to"<name> (importado)"(this Spanish suffix is hard-coded inpackage.rs, which departs from the rule that Rust never writes user-facing prose). -
Library data. Tags: each name is created or matched case-insensitively to an existing local tag; an existing tag keeps its local colour. Notes are applied if non-empty. The language override is applied only if it is one of the supported codes (
etiquetas::IDIOMAS), otherwise ignored. Game lists are saved again with a new id and their copy ids translated to the new card ids (nuevos, the map built while renumbering the cards); ids that are not in the deck are dropped (Game Table). -
Result. The new
Deckis returned and the library reloads. The command opens the file withBufReader.
| Threat | Defence |
|---|---|
Zip-slip (../x, /etc/passwd) |
enclosed_name; rejected with ruta (test una_ruta_con_puntos_se_rechaza). |
| Tampered or swapped image | Hash must match the name (test una_imagen_alterada_se_rechaza). Also protects against mislabelled files. |
| Zip bomb on a single entry |
MAX_ENTRADA = 512 MiB checked against the declared size and enforced while reading (take(MAX_ENTRADA + 1)); tamano error. |
Non-image payload under blobs/
|
probe in put_bytes rejects it. |
| Oversized images | 12000 px side cap in put_bytes. |
| Wrong file type |
format check; test sin_manifiesto_o_de_otro_formato_no_entra. |
| Package from a newer app |
Unsupported. |
| Deck referencing a missing image | Rejected up front (falta_imagen) and again by decks::save (blob_missing). |
Known gaps to be aware of (read from the code; Inferred where noted as risk):
- There is no cap on the number of entries or the total uncompressed size, and every entry is buffered in memory, so a crafted package with very many large entries can use a lot of memory. The 512 MiB per-entry cap is the only limit.
- Import is not transactional across steps. Images and collections are written before the deck is saved, and tags are applied after. If a later step fails, earlier images stay in the blob store as orphans (garbage collection removes them after the 24 h grace period, see Storage-and-Blobs) and a newly saved collection stays.
- The user sees a generic error text, because the screen translates by error code only (
src/lib/errores.ts, for exampleInvalidInputbecomes "Los datos no son válidos."); the specificmotivois not shown. See IPC-and-Error-Handling.
| Test | Guards |
|---|---|
ida_y_vuelta_en_un_equipo_limpio |
Round trip into an empty library, new ids, same blob set, second import renamed "(importado)". |
las_etiquetas_y_las_notas_viajan |
Tags merge with existing ones by name, notes and language. |
las_listas_para_jugar_viajan_con_los_ids_nuevos |
A game list travels, follows the new card ids and loses an id that was not in the deck. |
un_deck_disenado_viaja_con_su_arte_y_su_coleccion |
Designs, art, emblem and collection survive, and blob_refs protects the art from GC on the new machine. |
el_mismo_deck_da_el_mismo_fichero |
Deterministic output. |
una_imagen_alterada_se_rechaza |
Hash validation. |
una_ruta_con_puntos_se_rechaza |
Zip-slip. |
sin_manifiesto_o_de_otro_formato_no_entra |
Manifest and format checks. |
- Adding data to the package: make it an optional field with
#[serde(default)]inManifiestoorBiblioteca, fill it inescribir, apply it at the end ofimport. BumpVERSIONonly when an older app would lose or misread data (as designs did); otherwise keep it unchanged so older apps keep importing. - Adding a new place that holds image hashes: include it in
blobs_del_deck, otherwise the package will be incomplete and import will fail withfalta_imagen. - Hardening import: add total-size and entry-count caps in the loop of
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
- 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