Repository navigation
External Sources
English · Español
Everything the app downloads from outside Scryfall goes through one hardened HTTP client, fuentes::Externo (src-tauri/src/fuentes/mod.rs): a closed allow-list of sites and path prefixes, a re-check on every redirect, a byte cap enforced while streaming, and images that are always decoded and re-encoded. On top of it sit three data sources: Cockatrice's token database (cards Scryfall does not have), and two name indexes (MTGJSON offline, magicthegathering.io online) used to translate card names from other languages.
Boundaries: Scryfall itself has its own client with its own rules (Scryfall-Integration). Frame packs, fonts and themes use Externo too but are documented with their features (Frame-Packs, Themes-and-Skins).
| File | Responsibility |
|---|---|
src-tauri/src/fuentes/mod.rs |
PERMITIDOS, permitida(), url_permitida(), Externo (bajar, bajar_como_navegador, bajar_a_fichero), limpiar_imagen(), carta_cockatrice(), buscar_cockatrice(), nombres_ingleses(), resumen_cockatrice(), meta_cockatrice(). |
src-tauri/src/fuentes/cockatrice.rs |
Catalogo, FichaCockatrice, catalogo(): parses and indexes tokens.xml. |
src-tauri/src/fuentes/mtgjson.rs |
Offline other-language name index (SQLite), descargar(), info(), quitar(), nombres_ingleses(). |
src-tauri/src/fuentes/mtgio.rs |
Online name lookup against magicthegathering.io (last resort). |
src-tauri/src/commands/fuentes.rs |
Settings commands names_index_info, names_index_download, names_index_remove; event names://progress. |
src-tauri/src/commands/cards.rs |
card_fetch_externa, card_search/card_prints (append Cockatrice results), deck_tokens_detect. |
src-tauri/src/commands/identify.rs |
card_link_externa (metadata for linking a custom card to an external token). |
src-tauri/src/state.rs |
AppState.externo: Arc<Externo> (one client for the app). |
src/features/settings/nombres-idiomas.tsx |
Settings UI for the MTGJSON index (download, progress, remove). |
Why it exists (Inferred rationale from the code and its comments; the comments state what is enforced, not a threat model): several of these URLs come from third-party data (for example the image URLs inside Cockatrice's tokens.xml, or a redirect target chosen by a server), so the app must never be steerable to an arbitrary host, never buffer unbounded data, and never store attacker-controlled bytes verbatim.
Decision and mechanism, layer by layer:
-
Closed list (
PERMITIDOS). A URL is allowed only if the scheme ishttps, there is no explicit port, and the host equals an entry exactly and the path starts with that entry's prefix (permitida()). Exact host equality rejects look-alikes such asi.imgur.com.evil.com; the no-port rule rejects:8443. Test:solo_lo_de_la_lista. -
Redirects are re-checked.
Externo::nuevo()installsPolicy::custom: more than 3 hops is an error; a redirect to an allowed URL is followed; to a disallowed one the redirect is stopped, so the 3xx response comes back and is reported as a failure (non-success status).bajar_conalso re-checks the finalr.url(). -
Size cap, streamed. The
Content-Lengthheader is checked first when present; then the body is read in chunks and the download is cut as soon as it would exceedtope(InvalidInput,motivo = grande). A server that lies about or omits the length cannot make the app buffer more than the cap. -
Bytes are never stored as received. Images pass through
limpiar_imagen()(below): decode, then encode again. Catalogues are parsed and only the parsed data is kept; a downloaded file is only saved to disk if it parses (Catalogo::leer(&x)?beforeescribir_atomico). -
Integrity where a hash exists. MTGJSON's zip is checked against the SHA-256 that MTGJSON publishes next to it; fixed-commit downloads of frame packs and fonts check a per-file SHA-256 from the shipped manifests (
src-tauri/assets/packs-descargables.json,src-tauri/assets/letras-libres.json,src-tauri/crates/proxy-core/assets/marcos/original/imagenes.json). -
Fail soft. Every optional source degrades to "no result" (empty list,
Errlogged withtracing::warn!) instead of breaking the main flow.
| Host | Path prefix | Used by |
|---|---|---|
raw.githubusercontent.com |
/Cockatrice/Magic-Token/ |
Token catalogue (cockatrice.rs) |
raw.githubusercontent.com |
/SlightlyCircuitous/image-storage/ |
Token images that the catalogue links to |
cdn.staticneo.com |
/w/mtg/ |
Token images that the catalogue links to |
i.imgur.com |
/ |
Token images that the catalogue links to |
api.magicthegathering.io |
/v1/cards |
mtgio.rs |
raw.githubusercontent.com |
/PoolFlamingo/proxy-builder-packs/ |
Downloadable frame packs (diseno/descargables.rs) |
mtgjson.com |
/api/v5/ |
mtgjson.rs |
tweakcn.com |
/r/themes/ |
Theme import (temas.rs) |
fonts.googleapis.com |
/css |
Theme fonts, CSS (bajar_como_navegador) |
fonts.gstatic.com |
/s/ |
Theme fonts, woff2 files |
Adding a source means adding a row here first; there is no other way to reach the network through Externo. url_permitida() also accepts scheme-less //host/path (some catalogues use it) by prefixing https:.
For orientation, other outbound calls have their own guard:
| Caller | Mechanism |
|---|---|
scryfall/client.rs |
Fixed host api.scryfall.com, rate limiter, retries. Images only from URLs Scryfall returned. |
commands/cards.rs import_archidekt
|
Plain reqwest to archidekt.com/api/decks/{id}/ where {id} is a parsed integer (Decklist-Parsing). |
ocr.rs descargar_modelos
|
Plain reqwest to the OCR model host; no hash check (Card-Identification-and-OCR). |
diseno/original.rs, diseno/letras_libres.rs
|
Plain reqwest to a fixed commit on GitHub, SHA-256 verified per file. |
| Method | Behaviour |
|---|---|
Externo::nuevo() |
Builds the client: user agent ProxyBuilder/<version>, 20 s timeout, the redirect policy above. |
bajar(url, tope) -> Vec<u8> |
Allow-list check, GET, status check (NotFound for 404, Network otherwise), size cap, whole body in memory. |
bajar_como_navegador(url, tope) |
Same but sends a desktop-browser user agent; Google Fonts only serves woff2 to browsers it knows. |
bajar_a_fichero(url, ruta, tope, progreso) -> sha256 hex |
Streams to a file with a 600 s timeout, hashes while writing, reports (bytes, total?), deletes the partial file if the cap is exceeded. Used for the 52 MB MTGJSON zip. |
Constants: MAX_IMAGEN = 15 MiB, LADO_MAXIMO = 12 000 px, PNG_GRANDE = 2 MiB, output size 745 x 1040 (the size of Scryfall's PNGs).
-
imagesize::blob_sizereads the header only; failure ->ErrorCode::Image(motivo = no_es_imagen); either side above 12 000 px ->Image(demasiado_grande), so a decompression bomb is rejected before decoding. -
decode::decode(fromproxy-core::imaging) fully decodes it to RGBA. - If the input was a JPEG (magic bytes
FF D8), or it is "big" (more than 2 MiB or taller than 1500 px): resize to 745 x 1040 with Lanczos3 when larger than that, and encode JPEG quality 95. - Otherwise encode as PNG.
Note that step 3 resizes to the exact 745 x 1040 box (imageops::resize), i.e. a large image is forced to the card aspect rather than letterboxed (Inferred intent: the app stretches images to the card slot, see Imaging-and-PDF). Test: las_imagenes_se_vuelven_a_codificar (PNG stays PNG, JPEG stays JPEG, an HTML file is Image).
Cockatrice's Magic-Token repository (tokens.xml) lists roughly a thousand tokens; about 80 of them have no image on Scryfall (for example "Kelp", the 0/1 Plant Wall created by Wall of Kelp). Those are what the app imports.
-
Filter. While parsing, any token with at least one image URL containing
scryfall.iois dropped: Scryfall already serves it and it would appear twice. Tokens with no image at all are kept in the catalogue but never offered (every consumer checks!imagenes.is_empty()). -
FichaCockatrice:id,nombre,tipo,texto,fr(power/toughness),colores,imagenes: Vec<(set, url)>,creadores(thereverse-relatedcards that create it).linea_de_tipo()prefixes "Token " when missing, like Scryfall's type lines. -
Stable id. The XML has no ids, so
id= first 8 bytes (16 hex chars) of SHA-256 over name, type, text and P/T (huella). It must not change between downloads because decks store it inCardSource::Custom { externa: Some(RefExterna { fuente: Cockatrice, id }) }. -
Catalogue lifecycle (
catalogo()). OneArc<Catalogo>per session in a staticMutex<Option<...>>. Loaded fromapp_cache_dir/cockatrice-tokens.xmlif it is less than 7 days old; otherwise downloaded (cap 8 MiB), validated by parsing, then saved atomically. If the download fails the old file is used; with neither, the error propagates. The cache can be deleted freely. -
Indexes. By normalised name (
exactas) and by normalised creator (creadas_por, which tries the whole card name and each//face).buscar(texto)needs 3+ characters and ranks exact name, prefix, word-prefix, then substring. -
Parsing is tolerant (
roxmltree): missing fields become empty strings; invalid XML isInvalidInput(motivo = cockatrice), testun_xml_roto_no_vale. Fixture:src-tauri/src/fixtures/cockatrice-tokens.xml.
| Place | How |
|---|---|
| "Add card" search and variants |
card_search / card_prints append CardSummary rows with fuente = Cockatrice (thumbnail through pbimg://.../t/, downloaded by the protocol handler via Externo). Only for English / unset language and not in high-res-only mode, since Cockatrice images are not high resolution. |
| Resolving a list |
Guardar::externa_por_nombre (a line Scryfall could not find but Cockatrice has by exact name) and externas_creadas_por (tokens created by the deck's cards, when "add tokens" is on). Implemented in EnLaApp in commands/cards.rs. |
| "Detect tokens" |
deck_tokens_detect also proposes Cockatrice tokens created by cards in the deck, skipping names already present. |
| Linking own images |
identify.rs adds exact-name Cockatrice matches to the candidate list; card_link_externa returns the CardMeta for linking (Card-Identification-and-OCR). |
| Adding the card |
card_fetch_externa(fuente, id) -> carta_cockatrice(). |
For each image URL of the token, in order: Externo::bajar(url, MAX_IMAGEN), then limpiar_imagen, then blobs.put_bytes(..., ImageOrigin::Upload). The first success returns a DeckCard with CardSource::Custom { scryfall_id: None, externa: Some(...) }, front = the stored image, meta from meta_cockatrice (layout token, language en, set = first image's set code, type line). If every URL fails, the last error is returned, or NotFound with motivo = sin_imagen.
Design note: it is a custom card with an external reference rather than a new CardSource variant, so older app versions open the deck and see an ordinary custom card (Deck-Model). The exporter treats such cards as tokens and leaves them out of text lists (Decklist-Parsing).
Both sources answer one question: given a card name written in language xx ("Muro de algas", language es), what is the English name? The resolver then asks Scryfall for that English name. fuentes::nombres_ingleses(state, nombre, idioma) is the single entry point:
flowchart TD
A["nombres_ingleses(nombre, idioma)"] --> B{"MTGJSON index downloaded?"}
B -- yes --> C["mtgjson::nombres_ingleses<br/>SQLite lookup, may be empty"]
C --> R["return (no further fallback)"]
B -- no --> D["mtgio::nombres_ingleses<br/>HTTPS, 2 MiB cap, session memo"]
D --> R
Note the first branch: when the index exists but has no match, the answer is an empty list; magicthegathering.io is not consulted then.
Callers: resolve.rs step 1b (via Guardar::nombre_ingles, using the first result) and identify.rs when Scryfall finds nothing for the read/typed text.
-
What is stored. From
AtomicCards.json.zip(about 52 MB) only(normalised foreign name, Scryfall language code, English name)triples, inapp_cache_dir/mtgjson/nombres.sqlite(tablenombres, indexnombres_extranjero). MTGJSON has no tokens Scryfall lacks, so this is names only. Language names are mapped incodigo()(Spanish, French, German, Italian, Portuguese, Japanese, Korean, Russian, Chinese Simplified/Traditional, Phyrexian). -
Download pipeline (
descargar): fetchAtomicCards.json.zip.sha256(cap 1 KiB) -> stream the zip to a file withbajar_a_fichero(cap 120 MiB, progress phaseBajando) -> compare the SHA-256 with the published one (mismatch: delete the zip and fail withNetwork,motivo = huella) -> inspawn_blocking: open the zip, read entryAtomicCards.jsonwith a 1 GiB cap, collect the triples (leer_nombres, phaseLeyendo) -> write a temp SQLite file and rename it over the old one (escribir_base) -> delete the zip. The cached read-only connection is dropped (cerrar_base) before the replace. -
Lookup.
SELECT DISTINCT ingles FROM nombres WHERE extranjero = ?1 ORDER BY (idioma = ?2) DESC, ingles: names in the requested language first, but a match in any other language still counts. ReturnsNone(notSome(vec![])) when the file does not exist, which is what makes the caller fall back to the online source. -
Settings.
names_index_info->NombresIdiomasInfo { descargado, fecha, nombres, tamano }(tamanois a fixed 52 MiB estimate);names_index_download(marks a running job withstate.trabajo()and emitsnames://progresswith{ fase: bajando|leyendo, hecho, total });names_index_removedeletes the folder. UI:src/features/settings/nombres-idiomas.tsx. - Why optional: the download is large and most users need only English; without it the online fallback still works for lists that use other-language names.
- Query:
GET /v1/cards?name=<text>&language=<lang>&pageSize=10, mapping Scryfall codes to the API's language names (idioma_api; English has none, so it returns nothing). - Response is parsed leniently (
foreignNames); English names of cards whose foreign name equals the query come first, others after. - Results are memoised for the session (cleared at 2 000 entries). No network or a bad answer yields an empty list and a warning; the docs call it slow and unreliable, hence "last resort". Test:
el_nombre_ingles_de_la_exacta_primerowith fixturesrc-tauri/src/fixtures/mtgio-muro-de-algas.json.
| Condition | Result |
|---|---|
| URL not in the allow-list, bad scheme/port, malformed |
InvalidInput with motivo = host, url
|
| Body above the cap |
InvalidInput, motivo = grande
|
| HTTP 404 / other non-2xx / redirect refused |
NotFound / Network (with url, estado) |
| Not an image, or sides above 12 000 px |
Image (motivo = no_es_imagen / demasiado_grande) |
| Catalogue XML/zip unreadable |
InvalidInput (motivo = cockatrice / mtgjson) |
| MTGJSON hash mismatch |
Network (motivo = huella) |
| Optional lookup fails (Cockatrice catalogue, mtgio) | Logged, treated as no results |
src-tauri/src/fuentes/mod.rs (allow-list, re-encoding), cockatrice.rs (filtering, search, creators, broken XML), mtgio.rs (parsing), mtgjson.rs (indice_de_nombres: build, lookup, fallback language, remove). Live network tests exist but are #[ignore]d (en_vivo modules). The resolver paths that use these sources are tested with fakes in src-tauri/src/scryfall/resolve.rs.
- New downloadable resource: add its host/prefix to
PERMITIDOS(test it insolo_lo_de_la_lista), useExterno::bajarwith an explicit cap, never write the raw bytes: parse them, or pin and verify a SHA-256. - New image source: always route bytes through
limpiar_imagenbeforeblobs.put_bytes. - New card source: add a
FuenteCartavariant (src-tauri/crates/proxy-core/src/model.rs), mirrorcarta_cockatrice/meta_cockatrice/resumen_cockatrice, and extendcard_fetch_externaandcard_link_externa.
- Scryfall-Integration - the main source and how resolution uses these fallbacks.
- Card-Identification-and-OCR - linking custom cards, which also searches these sources.
-
Frame-Packs and Themes-and-Skins - other users of
Externo. - Storage-and-Blobs - where the cleaned images are stored and which caches are disposable.
- Settings-and-Shell - the Settings screen hosting the names index.
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