(theme): Evita referencias cíclicas de parent()

Como `Theme::parent()` se resuelve en tiempo de ejecución, una
referencia cíclica (en la que un tema acaba siendo padre de sí mismo,
directa o transitivamente) no puede descartarse en compilación. Se
detecta al registrar el tema y aborta el arranque si procede.
This commit is contained in:
Manuel Cillero 2026-08-09 18:45:17 +02:00
parent 825283caa3
commit 8cb5e035dc
3 changed files with 40 additions and 0 deletions

View file

@ -1,5 +1,6 @@
use crate::core::action::add_action; use crate::core::action::add_action;
use crate::core::extension::ExtensionRef; use crate::core::extension::ExtensionRef;
use crate::core::theme::ThemeRef;
use crate::core::theme::all::THEMES; use crate::core::theme::all::THEMES;
use crate::web::Router; use crate::web::Router;
use crate::{global, serve_static_files, trace, web}; use crate::{global, serve_static_files, trace, web};
@ -45,6 +46,8 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Comprueba si la extensión tiene un tema asociado que deba registrarse. // Comprueba si la extensión tiene un tema asociado que deba registrarse.
if let Some(theme) = extension.theme() { if let Some(theme) = extension.theme() {
check_theme_parent_chain(theme);
let mut registered_themes = THEMES.write(); let mut registered_themes = THEMES.write();
// Asegura que el tema no esté ya registrado para evitar duplicados. // Asegura que el tema no esté ya registrado para evitar duplicados.
if !registered_themes if !registered_themes
@ -60,6 +63,31 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
} }
} }
// Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se
// resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el
// arranque al detectar uno, antes de provocar un bucle infinito (en `Theme::handle_component()`) o
// un desbordamiento de pila (en los métodos predefinidos de `Theme` que delegan recursivamente en
// el tema padre).
fn check_theme_parent_chain(theme: ThemeRef) {
let mut chain: Vec<ThemeRef> = vec![theme];
let mut current = theme;
while let Some(parent) = current.parent() {
if let Some(pos) = chain.iter().position(|t| t.type_id() == parent.type_id()) {
let cycle: Vec<&str> = chain[pos..]
.iter()
.map(|t| t.short_name())
.chain(std::iter::once(parent.short_name()))
.collect();
panic!(
"Circular theme parent chain detected: {}",
cycle.join(" -> ")
);
}
chain.push(parent);
current = parent;
}
}
// **< REGISTRO DE LAS ACCIONES >******************************************************************* // **< REGISTRO DE LAS ACCIONES >*******************************************************************
pub fn register_actions() { pub fn register_actions() {

View file

@ -20,6 +20,13 @@
//! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez //! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez
//! padre de otro, basta declararlo cada vez con [`Theme::parent()`]. //! padre de otro, basta declararlo cada vez con [`Theme::parent()`].
//! //!
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
//! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese
//! ciclo provocaría un bucle infinito en [`Theme::handle_component()`] o un desbordamiento de pila
//! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo,
//! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación**
//! si detecta una referencia circular.
//!
//! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio //! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio
//! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa //! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa
//! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico //! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico

View file

@ -56,6 +56,11 @@ pub trait Theme: Extension + Send + Sync {
/// no los sobrescribe. /// no los sobrescribe.
/// ///
/// La implementación por defecto devuelve `None` (tema sin padre). /// La implementación por defecto devuelve `None` (tema sin padre).
///
/// Una referencia circular (un tema acaba siendo padre de sí mismo, directa o transitivamente)
/// no puede descartarse en tiempo de compilación. PageTop la detecta al registrar el tema y
/// aborta el arranque de la aplicación si encuentra una, para evitar bucles infinitos o
/// desbordamientos de pila al usar el tema.
fn parent(&self) -> Option<ThemeRef> { fn parent(&self) -> Option<ThemeRef> {
None None
} }