Skip to content

Fuentes externas

Pablo Martínez López edited this page Oct 20, 2026 · 1 revision

Fuentes externas

English · Español

Todo lo que la app descarga de fuera de Scryfall pasa por un único cliente HTTP reforzado, fuentes::Externo (src-tauri/src/fuentes/mod.rs): una lista cerrada de sitios y prefijos de ruta, una nueva comprobación en cada redirección, un tope de bytes aplicado mientras se lee en flujo y unas imágenes que siempre se decodifican y se vuelven a codificar. Encima hay tres fuentes de datos: la base de datos de fichas de Cockatrice (cartas que Scryfall no tiene) y dos índices de nombres (MTGJSON sin conexión y magicthegathering.io en línea) para traducir nombres de carta de otros idiomas.

Límites: Scryfall tiene su propio cliente con sus propias reglas (Integración con Scryfall). Los packs de marcos, las letras y los temas también usan Externo, pero se documentan con sus funcionalidades (Packs de marcos, Temas y skins).

Ficheros

Fichero Responsabilidad
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(): parsea e indexa tokens.xml.
src-tauri/src/fuentes/mtgjson.rs Índice de nombres en otros idiomas sin conexión (SQLite), descargar(), info(), quitar(), nombres_ingleses().
src-tauri/src/fuentes/mtgio.rs Búsqueda de nombres en línea contra magicthegathering.io (último recurso).
src-tauri/src/commands/fuentes.rs Comandos de Ajustes names_index_info, names_index_download, names_index_remove; evento names://progress.
src-tauri/src/commands/cards.rs card_fetch_externa, card_search/card_prints (añaden resultados de Cockatrice), deck_tokens_detect.
src-tauri/src/commands/identify.rs card_link_externa (metadatos para vincular una carta propia a una ficha externa).
src-tauri/src/state.rs AppState.externo: Arc<Externo> (un cliente para toda la app).
src/features/settings/nombres-idiomas.tsx Interfaz de Ajustes para el índice de MTGJSON (descarga, progreso, eliminación).

Modelo de seguridad

Por qué existe (justificación deducida del código y de sus comentarios; los comentarios dicen qué se aplica, no un modelo de amenazas): varias de estas URL vienen de datos de terceros (por ejemplo, las URL de imagen dentro del tokens.xml de Cockatrice, o un destino de redirección elegido por un servidor), así que la app nunca debe poder ser dirigida a un host arbitrario, nunca debe almacenar en memoria datos sin límite y nunca debe guardar tal cual bytes controlados por un atacante.

Decisión y mecanismo, capa por capa:

  1. Lista cerrada (PERMITIDOS). Una URL solo se permite si el esquema es https, no hay puerto explícito y el host es exactamente igual a una entrada y la ruta empieza por el prefijo de esa entrada (permitida()). La igualdad exacta del host rechaza imitaciones como i.imgur.com.evil.com; la regla de no puerto rechaza :8443. Test: solo_lo_de_la_lista.
  2. Las redirecciones se vuelven a comprobar. Externo::nuevo() instala Policy::custom: más de 3 saltos es un error; una redirección a una URL permitida se sigue; a una no permitida la redirección se detiene, así que la respuesta 3xx vuelve y se notifica como un fallo (estado no exitoso). bajar_con también vuelve a comprobar la r.url() final.
  3. Tope de tamaño, en flujo. Primero se comprueba la cabecera Content-Length cuando está presente; después el cuerpo se lee en trozos y la descarga se corta en cuanto superaría tope (InvalidInput, motivo = grande). Un servidor que mienta sobre la longitud, o que la omita, no puede hacer que la app almacene más del tope.
  4. Los bytes nunca se guardan tal como llegan. Las imágenes pasan por limpiar_imagen() (más abajo): decodificar y luego codificar de nuevo. Los catálogos se parsean y solo se conservan los datos parseados; un fichero descargado solo se guarda en disco si se parsea (Catalogo::leer(&x)? antes de escribir_atomico).
  5. Integridad donde hay un hash. El zip de MTGJSON se comprueba contra el SHA-256 que MTGJSON publica a su lado; las descargas de packs de marcos y letras con commit fijo comprueban un SHA-256 por fichero a partir de los manifiestos incluidos (src-tauri/assets/packs-descargables.json, src-tauri/assets/letras-libres.json, src-tauri/crates/proxy-core/assets/marcos/original/imagenes.json).
  6. Fallo suave. Toda fuente opcional se degrada a «sin resultado» (lista vacía, Err registrado con tracing::warn!) en lugar de romper el flujo principal.

La lista de permitidos

Host Prefijo de ruta Lo usa
raw.githubusercontent.com /Cockatrice/Magic-Token/ Catálogo de fichas (cockatrice.rs)
raw.githubusercontent.com /SlightlyCircuitous/image-storage/ Imágenes de fichas a las que enlaza el catálogo
cdn.staticneo.com /w/mtg/ Imágenes de fichas a las que enlaza el catálogo
i.imgur.com / Imágenes de fichas a las que enlaza el catálogo
api.magicthegathering.io /v1/cards mtgio.rs
raw.githubusercontent.com /PoolFlamingo/proxy-builder-packs/ Packs de marcos descargables (diseno/descargables.rs)
mtgjson.com /api/v5/ mtgjson.rs
tweakcn.com /r/themes/ Importación de temas (temas.rs)
fonts.googleapis.com /css Letras de los temas, CSS (bajar_como_navegador)
fonts.gstatic.com /s/ Letras de los temas, ficheros woff2

Añadir una fuente significa añadir primero una fila aquí; no hay otro modo de llegar a la red a través de Externo. url_permitida() también acepta //host/ruta sin esquema (algunos catálogos lo usan) anteponiendo https:.

Lo que no pasa por Externo

A modo de orientación, otras llamadas salientes tienen su propia protección:

Quien llama Mecanismo
scryfall/client.rs Host fijo api.scryfall.com, límite de ritmo, reintentos. Imágenes solo desde URL que devolvió Scryfall.
commands/cards.rs import_archidekt reqwest normal a archidekt.com/api/decks/{id}/ donde {id} es un entero parseado (Parseo de la lista del mazo).
ocr.rs descargar_modelos reqwest normal al host de los modelos de OCR; sin comprobación de hash (Identificación de cartas y OCR).
diseno/original.rs, diseno/letras_libres.rs reqwest normal a un commit fijo de GitHub, con SHA-256 verificado por fichero.

API de Externo

Método Comportamiento
Externo::nuevo() Construye el cliente: user agent ProxyBuilder/<version>, tiempo de espera de 20 s, la política de redirección de arriba.
bajar(url, tope) -> Vec<u8> Comprobación de la lista de permitidos, GET, comprobación del estado (NotFound para 404, Network en otro caso), tope de tamaño, cuerpo completo en memoria.
bajar_como_navegador(url, tope) Igual, pero envía un user agent de navegador de escritorio; Google Fonts solo sirve woff2 a los navegadores que conoce.
bajar_a_fichero(url, ruta, tope, progreso) -> sha256 hex Escribe en flujo a un fichero con un tiempo de espera de 600 s, calcula el hash mientras escribe, notifica (bytes, total?) y borra el fichero parcial si se supera el tope. Se usa para el zip de 52 MB de MTGJSON.

Constantes: MAX_IMAGEN = 15 MiB, LADO_MAXIMO = 12 000 px, PNG_GRANDE = 2 MiB, tamaño de salida 745 x 1040 (el tamaño de los PNG de Scryfall).

limpiar_imagen(bytes) - recodificación

  1. imagesize::blob_size lee solo la cabecera; si falla -> ErrorCode::Image (motivo = no_es_imagen); si cualquiera de los lados supera los 12 000 px -> Image (demasiado_grande), de modo que una bomba de descompresión se rechaza antes de decodificar.
  2. decode::decode (de proxy-core::imaging) la decodifica por completo a RGBA.
  3. Si la entrada era un JPEG (bytes mágicos FF D8), o es «grande» (más de 2 MiB o más de 1500 px de alto): se redimensiona a 745 x 1040 con Lanczos3 cuando es mayor que eso y se codifica como JPEG con calidad 95.
  4. En otro caso se codifica como PNG.

Obsérvese que el paso 3 redimensiona a la caja exacta de 745 x 1040 (imageops::resize), es decir, una imagen grande se fuerza a la proporción de la carta en lugar de dejar bandas (intención deducida: la app estira las imágenes a la casilla de la carta, véase Imagen y PDF). Test: las_imagenes_se_vuelven_a_codificar (un PNG sigue siendo PNG, un JPEG sigue siendo JPEG, un fichero HTML es Image).

Fichas de Cockatrice (cockatrice.rs)

El repositorio Magic-Token de Cockatrice (tokens.xml) lista aproximadamente un millar de fichas; unas 80 de ellas no tienen imagen en Scryfall (por ejemplo «Kelp», el Muro de plantas 0/1 que crea Wall of Kelp). Esas son las que importa la app.

  • Filtro. Al parsear, se descarta cualquier ficha con al menos una URL de imagen que contenga scryfall.io: Scryfall ya la sirve y aparecería dos veces. Las fichas sin ninguna imagen se conservan en el catálogo pero nunca se ofrecen (todos los consumidores comprueban !imagenes.is_empty()).
  • FichaCockatrice: id, nombre, tipo, texto, fr (fuerza/resistencia), colores, imagenes: Vec<(set, url)>, creadores (las cartas reverse-related que la crean). linea_de_tipo() antepone «Token » cuando falta, como las líneas de tipo de Scryfall.
  • Id estable. El XML no tiene ids, así que id = los primeros 8 bytes (16 caracteres hex) del SHA-256 de nombre, tipo, texto y F/R (huella). No debe cambiar entre descargas porque los mazos lo guardan en CardSource::Custom { externa: Some(RefExterna { fuente: Cockatrice, id }) }.
  • Ciclo de vida del catálogo (catalogo()). Un Arc<Catalogo> por sesión en un Mutex<Option<...>> estático. Se carga desde app_cache_dir/cockatrice-tokens.xml si tiene menos de 7 días; si no, se descarga (tope de 8 MiB), se valida parseándolo y se guarda de forma atómica. Si la descarga falla se usa el fichero viejo; si no hay ninguno, se propaga el error. La caché puede borrarse libremente.
  • Índices. Por nombre normalizado (exactas) y por creador normalizado (creadas_por, que prueba el nombre completo de la carta y cada cara separada por //). buscar(texto) necesita 3+ caracteres y ordena: nombre exacto, prefijo, prefijo de palabra y subcadena.
  • El parseo es tolerante (roxmltree): los campos que faltan pasan a ser cadenas vacías; un XML inválido es InvalidInput (motivo = cockatrice), test un_xml_roto_no_vale. Fixture: src-tauri/src/fixtures/cockatrice-tokens.xml.

Dónde aparecen las fichas de Cockatrice

Lugar Cómo
Búsqueda y variantes de «Añadir carta» card_search / card_prints añaden filas CardSummary con fuente = Cockatrice (miniatura mediante pbimg://.../t/, descargada por el manejador del protocolo a través de Externo). Solo para el idioma inglés / sin definir y no en el modo de solo alta resolución, ya que las imágenes de Cockatrice no son de alta resolución.
Resolver una lista Guardar::externa_por_nombre (una línea que Scryfall no encontró pero Cockatrice tiene por nombre exacto) y externas_creadas_por (fichas creadas por las cartas del mazo, cuando «añadir fichas» está activado). Implementado en EnLaApp en commands/cards.rs.
«Detectar fichas» deck_tokens_detect también propone fichas de Cockatrice creadas por cartas del mazo, saltándose los nombres ya presentes.
Vincular imágenes propias identify.rs añade a la lista de candidatas las coincidencias de nombre exacto de Cockatrice; card_link_externa devuelve el CardMeta para vincular (Identificación de cartas y OCR).
Añadir la carta card_fetch_externa(fuente, id) -> carta_cockatrice().

Convertir una entrada del catálogo en una carta del mazo (carta_cockatrice)

Para cada URL de imagen de la ficha, en orden: Externo::bajar(url, MAX_IMAGEN), luego limpiar_imagen, luego blobs.put_bytes(..., ImageOrigin::Upload). El primer éxito devuelve una DeckCard con CardSource::Custom { scryfall_id: None, externa: Some(...) }, front = la imagen almacenada, meta de meta_cockatrice (layout token, idioma en, set = código de set de la primera imagen, línea de tipo). Si todas las URL fallan, se devuelve el último error, o NotFound con motivo = sin_imagen.

Nota de diseño: es una carta propia (custom) con una referencia externa en lugar de una variante nueva de CardSource, de modo que las versiones antiguas de la app abren el mazo y ven una carta propia normal (Modelo del mazo). El exportador trata esas cartas como fichas y las deja fuera de las listas de texto (Parseo de la lista del mazo).

Nombres en otros idiomas

Ambas fuentes responden a una misma pregunta: dado un nombre de carta escrito en el idioma xx («Muro de algas», idioma es), ¿cuál es el nombre en inglés? Después el resolvedor pide a Scryfall ese nombre en inglés. fuentes::nombres_ingleses(state, nombre, idioma) es el único punto de entrada:

flowchart TD
    A["nombres_ingleses(nombre, idioma)"] --> B{"¿índice de MTGJSON descargado?"}
    B -- sí --> C["mtgjson::nombres_ingleses<br/>consulta SQLite, puede quedar vacía"]
    C --> R["devolver (sin más reservas)"]
    B -- no --> D["mtgio::nombres_ingleses<br/>HTTPS, tope de 2 MiB, memo de sesión"]
    D --> R
Loading

Obsérvese la primera rama: cuando el índice existe pero no tiene coincidencia, la respuesta es una lista vacía; entonces no se consulta magicthegathering.io.

Quienes llaman: resolve.rs paso 1b (mediante Guardar::nombre_ingles, usando el primer resultado) e identify.rs cuando Scryfall no encuentra nada para el texto leído o escrito.

Índice de MTGJSON (mtgjson.rs)

  • Qué se almacena. De AtomicCards.json.zip (unos 52 MB) solo las tripletas (nombre extranjero normalizado, código de idioma de Scryfall, nombre en inglés), en app_cache_dir/mtgjson/nombres.sqlite (tabla nombres, índice nombres_extranjero). MTGJSON no tiene fichas que Scryfall no tenga, así que son solo nombres. Los nombres de idioma se mapean en codigo() (español, francés, alemán, italiano, portugués, japonés, coreano, ruso, chino simplificado/tradicional, fyrexiano).
  • Flujo de descarga (descargar): pedir AtomicCards.json.zip.sha256 (tope de 1 KiB) -> escribir el zip en flujo a un fichero con bajar_a_fichero (tope de 120 MiB, fase de progreso Bajando) -> comparar el SHA-256 con el publicado (si no coincide: borrar el zip y fallar con Network, motivo = huella) -> en spawn_blocking: abrir el zip, leer la entrada AtomicCards.json con un tope de 1 GiB, reunir las tripletas (leer_nombres, fase Leyendo) -> escribir un fichero SQLite temporal y renombrarlo sobre el viejo (escribir_base) -> borrar el zip. La conexión de solo lectura en caché se cierra (cerrar_base) antes del reemplazo.
  • Búsqueda. SELECT DISTINCT ingles FROM nombres WHERE extranjero = ?1 ORDER BY (idioma = ?2) DESC, ingles: primero los nombres en el idioma pedido, pero una coincidencia en cualquier otro idioma también cuenta. Devuelve None (no Some(vec![])) cuando el fichero no existe, que es lo que hace que quien llama recurra a la fuente en línea.
  • Ajustes. names_index_info -> NombresIdiomasInfo { descargado, fecha, nombres, tamano } (tamano es una estimación fija de 52 MiB); names_index_download (marca un trabajo en marcha con state.trabajo() y emite names://progress con { fase: bajando|leyendo, hecho, total }); names_index_remove borra la carpeta. Interfaz: src/features/settings/nombres-idiomas.tsx.
  • Por qué es opcional: la descarga es grande y la mayoría de los usuarios solo necesitan inglés; sin ella, la reserva en línea sigue funcionando para las listas que usan nombres en otros idiomas.

magicthegathering.io (mtgio.rs)

  • Consulta: GET /v1/cards?name=<text>&language=<lang>&pageSize=10, mapeando los códigos de Scryfall a los nombres de idioma de la API (idioma_api; el inglés no tiene, así que no devuelve nada).
  • La respuesta se parsea de forma tolerante (foreignNames); los nombres en inglés de las cartas cuyo nombre extranjero es igual a la consulta van primero, los demás después.
  • Los resultados se memorizan durante la sesión (se vacían al llegar a 2 000 entradas). Sin red o con una respuesta incorrecta, se obtiene una lista vacía y un aviso; la documentación la califica de lenta y poco fiable, de ahí «último recurso». Test: el_nombre_ingles_de_la_exacta_primero con el fixture src-tauri/src/fixtures/mtgio-muro-de-algas.json.

Modos de fallo

Condición Resultado
URL fuera de la lista de permitidos, esquema/puerto incorrecto, mal formada InvalidInput con motivo = host, url
Cuerpo por encima del tope InvalidInput, motivo = grande
HTTP 404 / otro no 2xx / redirección rechazada NotFound / Network (con url, estado)
No es una imagen, o lados por encima de 12 000 px Image (motivo = no_es_imagen / demasiado_grande)
XML/zip del catálogo ilegible InvalidInput (motivo = cockatrice / mtgjson)
Hash de MTGJSON no coincide Network (motivo = huella)
Falla una consulta opcional (catálogo de Cockatrice, mtgio) Se registra, se trata como sin resultados

Tests

src-tauri/src/fuentes/mod.rs (lista de permitidos, recodificación), cockatrice.rs (filtrado, búsqueda, creadores, XML roto), mtgio.rs (parseo), mtgjson.rs (indice_de_nombres: construcción, búsqueda, idioma de reserva, eliminación). Existen tests de red en vivo pero están marcados #[ignore] (módulos en_vivo). Los caminos del resolvedor que usan estas fuentes se prueban con falsos en src-tauri/src/scryfall/resolve.rs.

Por dónde empezar para ampliarlo

  • Nuevo recurso descargable: añade su host/prefijo a PERMITIDOS (pruébalo en solo_lo_de_la_lista), usa Externo::bajar con un tope explícito y nunca escribas los bytes en bruto: parsea o fija y verifica un SHA-256.
  • Nueva fuente de imágenes: pasa siempre los bytes por limpiar_imagen antes de blobs.put_bytes.
  • Nueva fuente de cartas: añade una variante de FuenteCarta (src-tauri/crates/proxy-core/src/model.rs), imita carta_cockatrice / meta_cockatrice / resumen_cockatrice y amplía card_fetch_externa y card_link_externa.

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally