Compare commits
2 Commits
27690c3afa
...
b7ce079a26
| Author | SHA1 | Date | |
|---|---|---|---|
| b7ce079a26 | |||
| 171b790fa3 |
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
96
src/shader/ShaderUiPath.cpp
Normal file
96
src/shader/ShaderUiPath.cpp
Normal 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
14
src/shader/ShaderUiPath.h
Normal 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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user