2 Commits

Author SHA1 Message Date
b7ce079a26 Fixed duplication
All checks were successful
CI / React UI Build (push) Successful in 39s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 3m6s
2026-06-01 06:52:56 -04:00
171b790fa3 Fixed bindings 2026-06-01 06:45:41 -04:00
8 changed files with 182 additions and 125 deletions

View File

@@ -136,6 +136,7 @@ set(SHADER_MANIFEST_SOURCES
"${SRC_DIR}/shader/ShaderManifestParameters.cpp" "${SRC_DIR}/shader/ShaderManifestParameters.cpp"
"${SRC_DIR}/shader/ShaderManifestParser.cpp" "${SRC_DIR}/shader/ShaderManifestParser.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp" "${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${SRC_DIR}/shader/ShaderUiPath.cpp"
) )
set(VIDEO_MODE_SOURCES set(VIDEO_MODE_SOURCES

View File

@@ -2,73 +2,12 @@
#include "../control/RuntimeControlCommand.h" #include "../control/RuntimeControlCommand.h"
#include "../control/RuntimeStateJson.h" #include "../control/RuntimeStateJson.h"
#include "../shader/ShaderUiPath.h"
#include <utility> #include <utility>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
namespace
{
bool IsSafeShaderAssetPath(const std::string& assetPath, std::filesystem::path& normalizedPath)
{
if (assetPath.empty() || assetPath.find('\\') != std::string::npos ||
assetPath.find(':') != std::string::npos || assetPath.find('?') != std::string::npos ||
assetPath.find('#') != std::string::npos)
return false;
const std::filesystem::path path(assetPath);
if (path.empty() || path.is_absolute())
return false;
bool firstPart = true;
bool startsInUiDirectory = false;
for (const std::filesystem::path& part : path)
{
if (part.empty() || part == "." || part == "..")
return false;
if (firstPart)
{
startsInUiDirectory = part == "ui";
firstPart = false;
}
}
if (!startsInUiDirectory)
return false;
normalizedPath = path.lexically_normal();
if (normalizedPath.empty() || normalizedPath.is_absolute())
return false;
for (const std::filesystem::path& part : normalizedPath)
{
if (part.empty() || part == "." || part == "..")
return false;
}
return true;
}
bool IsPathUnderRoot(const std::filesystem::path& root, const std::filesystem::path& path)
{
std::error_code errorCode;
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, errorCode);
if (errorCode)
return false;
const std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(path, errorCode);
if (errorCode)
return false;
const std::filesystem::path relative = canonicalPath.lexically_relative(canonicalRoot);
if (relative.empty() || relative.is_absolute())
return false;
for (const std::filesystem::path& part : relative)
{
if (part == "..")
return false;
}
return true;
}
}
ShaderRuntimeContentController::ShaderRuntimeContentController(RenderLayerPublisher publisher) : ShaderRuntimeContentController::ShaderRuntimeContentController(RenderLayerPublisher publisher) :
mRuntimeLayers(std::move(publisher)) mRuntimeLayers(std::move(publisher))
{ {
@@ -145,26 +84,15 @@ bool ShaderRuntimeContentController::ResolveAssetPath(
return false; return false;
} }
if (!ShaderUiPath::ResolveAssetPath(shaderPackage->directoryPath, assetPath, resolvedPath))
{
std::filesystem::path normalizedPath; std::filesystem::path normalizedPath;
if (!IsSafeShaderAssetPath(assetPath, normalizedPath)) if (!ShaderUiPath::NormalizeAssetPath(assetPath, normalizedPath))
{
error = "Shader asset path is not safe."; error = "Shader asset path is not safe.";
return false; else
}
const std::filesystem::path candidatePath = shaderPackage->directoryPath / normalizedPath;
if (!IsPathUnderRoot(shaderPackage->directoryPath, candidatePath))
{
error = "Shader asset path escaped the package directory.";
return false;
}
if (!std::filesystem::exists(candidatePath) || !std::filesystem::is_regular_file(candidatePath))
{
error = "Shader asset was not found."; error = "Shader asset was not found.";
return false; return false;
} }
resolvedPath = candidatePath;
return true; return true;
} }
} }

View File

@@ -1,6 +1,8 @@
#include "stdafx.h" #include "stdafx.h"
#include "ShaderManifestParser.h" #include "ShaderManifestParser.h"
#include "ShaderUiPath.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cctype> #include <cctype>
@@ -47,46 +49,6 @@ bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySo
return false; return false;
} }
bool IsSafeUiEntryPath(const std::string& entryPath)
{
if (Trim(entryPath).empty())
return false;
if (entryPath.find('\\') != std::string::npos || entryPath.find(':') != std::string::npos ||
entryPath.find('?') != std::string::npos || entryPath.find('#') != std::string::npos)
return false;
const std::filesystem::path path(entryPath);
if (path.empty() || path.is_absolute())
return false;
bool firstPart = true;
bool startsInUiDirectory = false;
for (const std::filesystem::path& part : path)
{
if (part.empty() || part == "." || part == "..")
return false;
if (firstPart)
{
startsInUiDirectory = part == "ui";
firstPart = false;
}
}
if (!startsInUiDirectory)
return false;
const std::filesystem::path normalized = path.lexically_normal();
if (normalized.empty() || normalized.is_absolute())
return false;
for (const std::filesystem::path& part : normalized)
{
if (part.empty() || part == "." || part == "..")
return false;
}
const std::string extension = normalized.extension().string();
return extension == ".js" || extension == ".mjs";
}
bool IsValidCustomElementTag(const std::string& tag) bool IsValidCustomElementTag(const std::string& tag)
{ {
if (tag.empty() || tag.find('-') == std::string::npos || tag.front() == '-' || tag.back() == '-') if (tag.empty() || tag.find('-') == std::string::npos || tag.front() == '-' || tag.back() == '-')
@@ -450,7 +412,9 @@ bool ParseUiDefinition(const JsonValue& manifestJson, ShaderPackage& shaderPacka
error = "Shader UI type must be 'webComponent' in: " + ManifestPathMessage(manifestPath); error = "Shader UI type must be 'webComponent' in: " + ManifestPathMessage(manifestPath);
return false; return false;
} }
if (!IsSafeUiEntryPath(ui.entryPath)) std::filesystem::path normalizedEntryPath;
if (!ShaderUiPath::NormalizeAssetPath(ui.entryPath, normalizedEntryPath) ||
!ShaderUiPath::IsModulePath(normalizedEntryPath))
{ {
error = "Shader UI entry must be a safe relative .js or .mjs path under ui/ in: " + ManifestPathMessage(manifestPath); error = "Shader UI entry must be a safe relative .js or .mjs path under ui/ in: " + ManifestPathMessage(manifestPath);
return false; return false;
@@ -461,14 +425,15 @@ bool ParseUiDefinition(const JsonValue& manifestJson, ShaderPackage& shaderPacka
return false; return false;
} }
const std::filesystem::path entryPath = shaderPackage.directoryPath / std::filesystem::path(ui.entryPath); std::filesystem::path entryPath;
if (!std::filesystem::exists(entryPath)) if (!ShaderUiPath::ResolveAssetPath(shaderPackage.directoryPath, ui.entryPath, entryPath))
{ {
error = "Shader UI entry not found for package " + shaderPackage.id + ": " + entryPath.string(); error = "Shader UI entry not found for package " + shaderPackage.id + ": " +
(shaderPackage.directoryPath / normalizedEntryPath).string();
return false; return false;
} }
ui.entryPath = std::filesystem::path(ui.entryPath).lexically_normal().generic_string(); ui.entryPath = normalizedEntryPath.generic_string();
ui.enabled = true; ui.enabled = true;
shaderPackage.ui = ui; shaderPackage.ui = ui;
return true; return true;

View File

@@ -77,6 +77,7 @@ bool ShaderPackageRegistry::Scan(
return false; return false;
} }
std::map<std::string, std::string> customUiTagOwners;
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot)) for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
{ {
if (!entry.is_directory()) if (!entry.is_directory())
@@ -100,6 +101,21 @@ bool ShaderPackageRegistry::Scan(
continue; continue;
} }
if (shaderPackage.ui.enabled)
{
const auto tagOwnerIt = customUiTagOwners.find(shaderPackage.ui.customElementTag);
if (tagOwnerIt != customUiTagOwners.end())
{
packageStatuses.push_back(BuildUnavailableStatus(
manifestPath,
shaderPackage,
"Duplicate shader UI custom element tag '" + shaderPackage.ui.customElementTag +
"' already used by shader '" + tagOwnerIt->second + "'."));
continue;
}
customUiTagOwners[shaderPackage.ui.customElementTag] = shaderPackage.id;
}
packageOrder.push_back(shaderPackage.id); packageOrder.push_back(shaderPackage.id);
packageStatuses.push_back(BuildAvailableStatus(shaderPackage)); packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
packagesById[shaderPackage.id] = shaderPackage; packagesById[shaderPackage.id] = shaderPackage;

View File

@@ -0,0 +1,96 @@
#include "stdafx.h"
#include "ShaderUiPath.h"
namespace ShaderUiPath
{
namespace
{
bool IsPathUnderRoot(const std::filesystem::path& root, const std::filesystem::path& path)
{
std::error_code errorCode;
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, errorCode);
if (errorCode)
return false;
const std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(path, errorCode);
if (errorCode)
return false;
const std::filesystem::path relative = canonicalPath.lexically_relative(canonicalRoot);
if (relative.empty() || relative.is_absolute())
return false;
for (const std::filesystem::path& part : relative)
{
if (part == "..")
return false;
}
return true;
}
}
bool NormalizeAssetPath(const std::string& assetPath, std::filesystem::path& normalizedPath)
{
normalizedPath.clear();
if (assetPath.empty() || assetPath.find('\\') != std::string::npos ||
assetPath.find(':') != std::string::npos || assetPath.find('?') != std::string::npos ||
assetPath.find('#') != std::string::npos)
{
return false;
}
const std::filesystem::path path(assetPath);
if (path.empty() || path.is_absolute())
return false;
bool firstPart = true;
bool startsInUiDirectory = false;
for (const std::filesystem::path& part : path)
{
if (part.empty() || part == "." || part == "..")
return false;
if (firstPart)
{
startsInUiDirectory = part == "ui";
firstPart = false;
}
}
if (!startsInUiDirectory)
return false;
normalizedPath = path.lexically_normal();
if (normalizedPath.empty() || normalizedPath.is_absolute())
return false;
for (const std::filesystem::path& part : normalizedPath)
{
if (part.empty() || part == "." || part == "..")
return false;
}
return true;
}
bool IsModulePath(const std::filesystem::path& normalizedPath)
{
const std::string extension = normalizedPath.extension().string();
return extension == ".js" || extension == ".mjs";
}
bool ResolveAssetPath(
const std::filesystem::path& packageRoot,
const std::string& assetPath,
std::filesystem::path& resolvedPath)
{
resolvedPath.clear();
std::filesystem::path normalizedPath;
if (!NormalizeAssetPath(assetPath, normalizedPath))
return false;
const std::filesystem::path candidatePath = packageRoot / normalizedPath;
if (!IsPathUnderRoot(packageRoot, candidatePath))
return false;
if (!std::filesystem::exists(candidatePath) || !std::filesystem::is_regular_file(candidatePath))
return false;
resolvedPath = candidatePath;
return true;
}
}

14
src/shader/ShaderUiPath.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <filesystem>
#include <string>
namespace ShaderUiPath
{
bool NormalizeAssetPath(const std::string& assetPath, std::filesystem::path& normalizedPath);
bool IsModulePath(const std::filesystem::path& normalizedPath);
bool ResolveAssetPath(
const std::filesystem::path& packageRoot,
const std::string& assetPath,
std::filesystem::path& resolvedPath);
}

View File

@@ -138,6 +138,38 @@ void TestInvalidUiDefinition()
std::filesystem::remove_all(root); std::filesystem::remove_all(root);
} }
void TestDuplicateCustomUiTags()
{
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "a" / "ui" / "controls.js", "customElements.define('shared-controls', class extends HTMLElement {});\n");
WriteFile(root / "b" / "ui" / "controls.js", "customElements.define('shared-controls', class extends HTMLElement {});\n");
WriteShaderPackage(root, "a", R"({
"id": "shader-a",
"name": "Shader A",
"ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "shared-controls" },
"parameters": []
})");
WriteShaderPackage(root, "b", R"({
"id": "shader-b",
"name": "Shader B",
"ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "shared-controls" },
"parameters": []
})");
ShaderPackageRegistry registry(4);
std::map<std::string, ShaderPackage> packages;
std::vector<std::string> order;
std::vector<ShaderPackageStatus> statuses;
std::string error;
Expect(registry.Scan(root, packages, order, statuses, error), "duplicate custom UI tags do not fail the whole scan");
Expect(packages.size() == 1, "only one duplicate custom UI tag owner remains available");
Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) {
return !status.available && status.error.find("Duplicate shader UI custom element tag") != std::string::npos;
}), "duplicate custom UI tag is surfaced as an unavailable shader");
std::filesystem::remove_all(root);
}
void TestExplicitPassManifest() void TestExplicitPassManifest()
{ {
const std::filesystem::path root = MakeTestRoot(); const std::filesystem::path root = MakeTestRoot();
@@ -335,6 +367,7 @@ int main()
TestValidManifest(); TestValidManifest();
TestExplicitPassManifest(); TestExplicitPassManifest();
TestInvalidUiDefinition(); TestInvalidUiDefinition();
TestDuplicateCustomUiTags();
TestMissingFontAsset(); TestMissingFontAsset();
TestInvalidManifest(); TestInvalidManifest();
TestInvalidTemporalSettings(); TestInvalidTemporalSettings();

View File

@@ -140,7 +140,11 @@ export function ShaderCustomPanel({ layer, ui, onParameterChange, onResetParamet
return; return;
} }
if (element.parentNode !== node) { if (element.parentNode !== node) {
try {
node.replaceChildren(element); node.replaceChildren(element);
} catch (error) {
setLoadError(error instanceof Error ? error.message : "Custom controls failed to mount.");
}
} }
}} }}
> >