refactor(ui): restrict external asset overrides to development mode

This commit is contained in:
Claudecio Martins
2026-10-20 20:07:52 -03:00
parent 5d8c5d487d
commit cf63aa6d22
33 changed files with 242 additions and 282 deletions
@@ -95,6 +95,10 @@ public class CyberCoreDesktopApplication extends Application {
developmentMode = devMode;
}
public static boolean isDevelopmentMode() {
return developmentMode;
}
public static MainAppView getMainAppView() {
return mainAppView;
}
@@ -39,15 +39,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import br.com.cybercore.signer.ui.CyberCoreDesktopApplication;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Gerenciador centralizado de recursos visuais (ícones e imagens).
* Permite que o usuário customize ou substitua facilmente qualquer ícone ou imagem (SVG/PNG)
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
* Centraliza o carregamento de recursos do classpath em produção e permite
* substituição via pasta externa 'assets/' exclusivamente em modo de desenvolvimento.
*/
public final class AssetManager {
@@ -55,9 +55,33 @@ public final class AssetManager {
private static final String[] RASTER_EXTENSIONS = {".png", ".jpg", ".jpeg"};
private static volatile Boolean externalOverridesOverride = null;
private AssetManager() {
}
/**
* Define explicitamente se o carregamento de assets externos está habilitado (útil para testes).
*/
public static void setExternalOverridesEnabled(Boolean enabled) {
externalOverridesOverride = enabled;
}
/**
* Indica se o carregamento de assets externos (./assets ou ~/.cybercore/assets) é permitido.
* Em ambiente de produção, este método retorna false por padrão para garantir
* integridade e isolamento estrito de recursos empacotados.
*/
public static boolean isExternalOverridesAllowed() {
if (externalOverridesOverride != null) {
return externalOverridesOverride;
}
if (CyberCoreDesktopApplication.isDevelopmentMode()) {
return true;
}
return Boolean.getBoolean("cybercore.dev") || Boolean.getBoolean("cybercore.assets.external");
}
/**
* Retorna o caminho do diretório externo prioritário de assets.
*/
@@ -82,31 +106,33 @@ public final class AssetManager {
String cleanName = cleanBaseName(baseName);
// 1. Procura no diretório local ./assets/{category}/
Path localDir = Path.of("assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = localDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap do diretório local: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage());
if (isExternalOverridesAllowed()) {
// 1. Procura no diretório local ./assets/{category}/
Path localDir = Path.of("assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = localDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap do diretório local: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage());
}
}
}
}
// 2. Procura em ~/.cybercore/assets/{category}/
String userHome = System.getProperty("user.home", ".");
Path userDir = Path.of(userHome, ".cybercore", "assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = userDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage());
// 2. Procura em ~/.cybercore/assets/{category}/
String userHome = System.getProperty("user.home", ".");
Path userDir = Path.of(userHome, ".cybercore", "assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = userDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage());
}
}
}
}
@@ -141,17 +167,19 @@ public final class AssetManager {
if (iconName != null && !iconName.isBlank()) {
String cleanName = cleanBaseName(iconName);
// 1. Prioridade para SVG em ./assets/icons/
Optional<Node> localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size, tint);
if (localSvg.isPresent()) {
return localSvg.get();
}
if (isExternalOverridesAllowed()) {
// 1. Prioridade para SVG em ./assets/icons/
Optional<Node> localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size, tint);
if (localSvg.isPresent()) {
return localSvg.get();
}
// 2. Procura SVG em ~/.cybercore/assets/icons/
String userHome = System.getProperty("user.home", ".");
Optional<Node> userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size, tint);
if (userSvg.isPresent()) {
return userSvg.get();
// 2. Procura SVG em ~/.cybercore/assets/icons/
String userHome = System.getProperty("user.home", ".");
Optional<Node> userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size, tint);
if (userSvg.isPresent()) {
return userSvg.get();
}
}
// 3. Procura SVG no Classpath interno
@@ -188,32 +216,34 @@ public final class AssetManager {
if (imageName != null && !imageName.isBlank()) {
String cleanName = cleanBaseName(imageName);
// 1. Prioridade para SVG em ./assets/images/
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
if (Files.isRegularFile(localSvg)) {
try (InputStream in = new FileInputStream(localSvg.toFile())) {
Node node = parseSvgToNode(in, width, height, null);
if (node != null) {
logger.debug("Carregada imagem SVG local: {}", localSvg);
return node;
if (isExternalOverridesAllowed()) {
// 1. Prioridade para SVG em ./assets/images/
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
if (Files.isRegularFile(localSvg)) {
try (InputStream in = new FileInputStream(localSvg.toFile())) {
Node node = parseSvgToNode(in, width, height, null);
if (node != null) {
logger.debug("Carregada imagem SVG local: {}", localSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
}
}
// 2. Procura SVG em ~/.cybercore/assets/images/
String userHome = System.getProperty("user.home", ".");
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
if (Files.isRegularFile(userSvg)) {
try (InputStream in = new FileInputStream(userSvg.toFile())) {
Node node = parseSvgToNode(in, width, height, null);
if (node != null) {
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
return node;
// 2. Procura SVG em ~/.cybercore/assets/images/
String userHome = System.getProperty("user.home", ".");
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
if (Files.isRegularFile(userSvg)) {
try (InputStream in = new FileInputStream(userSvg.toFile())) {
Node node = parseSvgToNode(in, width, height, null);
if (node != null) {
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
}
}
@@ -82,20 +82,26 @@ class AssetManagerTest {
}
@Test
@DisplayName("Deve interpretar atributos style inline no padrão de exportação do Inkscape/Figma")
void shouldParseSvgWithInlineStyles() {
String svgWithStyle = """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect x="10" y="10" width="28" height="28" style="fill:#d32f2f;stroke:#0288d1;stroke-width:2.5;opacity:0.9"/>
<text x="24" y="24" style="fill:#ffffff;font-size:10px">OK</text>
</svg>
""";
try (var in = new java.io.ByteArrayInputStream(svgWithStyle.getBytes(java.nio.charset.StandardCharsets.UTF_8))) {
Node node = AssetManager.parseSvgToNode(in, 48, 48);
assertThat(node).isNotNull();
assertThat(node).isInstanceOf(javafx.scene.layout.Pane.class);
} catch (Exception e) {
org.junit.jupiter.api.Assertions.fail("Falha ao analisar SVG com estilo inline: " + e.getMessage());
@DisplayName("Deve restringir overrides externos ao modo de desenvolvimento e isolar recursos em produção")
void shouldIsolateAssetsInProductionMode() {
try {
// Em produção (overrides desabilitados):
AssetManager.setExternalOverridesEnabled(false);
assertThat(AssetManager.isExternalOverridesAllowed()).isFalse();
// Ícones devem carregar do classpath mesmo sem diretório externo
Node certIcon = AssetManager.getIconOrDefault("cert", 24, null);
assertThat(certIcon).isNotNull();
Node logoNode = AssetManager.getImageOrDefault("app-logo", 200, 60, null);
assertThat(logoNode).isNotNull();
// Em modo desenvolvimento explicitamente habilitado:
AssetManager.setExternalOverridesEnabled(true);
assertThat(AssetManager.isExternalOverridesAllowed()).isTrue();
} finally {
AssetManager.setExternalOverridesEnabled(null);
}
}
}