mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Icons] Add SVG currentColor sentinel resolver (#7593)
Qt's SVG renderer has no CSS cascade, so a `currentColor` fill has to be substituted before the bytes reach QSvgRenderer. - Match and replace on bytes: both the sentinel and the replacement are ASCII, so the document never goes through a UTF-16 round trip, and files that are not valid UTF-8 survive unchanged. - Document that the match is a plain case-insensitive substring, so an identifier that merely starts with the sentinel gets rewritten too. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
7b4827dadd
commit
b04d8b88ac
5 changed files with 144 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ set(UTILITY_SOURCES
|
||||||
libcockatrice/utility/passwordhasher.cpp
|
libcockatrice/utility/passwordhasher.cpp
|
||||||
libcockatrice/utility/report_categories.cpp
|
libcockatrice/utility/report_categories.cpp
|
||||||
libcockatrice/utility/server_rate_limiter.cpp
|
libcockatrice/utility/server_rate_limiter.cpp
|
||||||
|
libcockatrice/utility/svg_recolor.cpp
|
||||||
libcockatrice/utility/translation_loader.cpp
|
libcockatrice/utility/translation_loader.cpp
|
||||||
libcockatrice/utility/user_data_backup.cpp
|
libcockatrice/utility/user_data_backup.cpp
|
||||||
libcockatrice/utility/warning_categories.cpp
|
libcockatrice/utility/warning_categories.cpp
|
||||||
|
|
@ -34,6 +35,7 @@ set(UTILITY_HEADERS
|
||||||
libcockatrice/utility/zone_names.h
|
libcockatrice/utility/zone_names.h
|
||||||
libcockatrice/utility/days_years_between.h
|
libcockatrice/utility/days_years_between.h
|
||||||
libcockatrice/utility/server_rate_limiter.h
|
libcockatrice/utility/server_rate_limiter.h
|
||||||
|
libcockatrice/utility/svg_recolor.h
|
||||||
libcockatrice/utility/translation_loader.h
|
libcockatrice/utility/translation_loader.h
|
||||||
libcockatrice/utility/user_data_backup.h
|
libcockatrice/utility/user_data_backup.h
|
||||||
libcockatrice/utility/warning_categories.h
|
libcockatrice/utility/warning_categories.h
|
||||||
|
|
|
||||||
44
libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp
Normal file
44
libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include "svg_recolor.h"
|
||||||
|
|
||||||
|
#include <QByteArrayView>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr QByteArrayView SENTINEL = "currentColor";
|
||||||
|
|
||||||
|
/// Case-insensitive byte search for the sentinel, starting at @p from.
|
||||||
|
/// QByteArray::indexOf() only matches case-sensitively for QByteArrayView, and
|
||||||
|
/// its char overload cannot match a word.
|
||||||
|
qsizetype findSentinel(const QByteArray &bytes, qsizetype from)
|
||||||
|
{
|
||||||
|
// QByteArrayView::sliced() is a view, so the scan does not allocate a copy
|
||||||
|
// at every byte position (QByteArray::sliced() would).
|
||||||
|
const QByteArrayView view(bytes);
|
||||||
|
for (qsizetype i = from; i + SENTINEL.size() <= view.size(); ++i) {
|
||||||
|
if (view.sliced(i, SENTINEL.size()).compare(SENTINEL, Qt::CaseInsensitive) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color)
|
||||||
|
{
|
||||||
|
// The sentinel and the replacement are both ASCII, so stay in bytes: a
|
||||||
|
// QString round trip would decode the whole document (and mangle bytes that
|
||||||
|
// are not valid UTF-8) even on the no-op path.
|
||||||
|
qsizetype pos = findSentinel(svg, 0);
|
||||||
|
if (pos < 0) {
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray resolved = svg;
|
||||||
|
while (pos >= 0) {
|
||||||
|
resolved.replace(pos, SENTINEL.size(), color);
|
||||||
|
// Skip past the inserted color: a caller's color never contains the
|
||||||
|
// sentinel, and this keeps the scan linear.
|
||||||
|
pos = findSentinel(resolved, pos + color.size());
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
31
libcockatrice_utility/libcockatrice/utility/svg_recolor.h
Normal file
31
libcockatrice_utility/libcockatrice/utility/svg_recolor.h
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* @file svg_recolor.h
|
||||||
|
* @ingroup Core
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SVG_RECOLOR_H
|
||||||
|
#define SVG_RECOLOR_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolves the `currentColor` sentinel in authored SVG data.
|
||||||
|
*
|
||||||
|
* Qt's SVG renderer has no CSS cascade or ambient color context, so the
|
||||||
|
* sentinel is not resolved by Qt. Callers pass the concrete color they want
|
||||||
|
* (for example a QPalette role serialized to #RRGGBB) and receive SVG bytes
|
||||||
|
* that are safe to hand to QSvgRenderer.
|
||||||
|
*
|
||||||
|
* Matching is a plain, case-insensitive substring search, not a CSS parse: an
|
||||||
|
* identifier that merely starts with the sentinel (for example
|
||||||
|
* `id="currentColorStop"`) is rewritten too. That is acceptable for the assets
|
||||||
|
* this project authors, and keeps the resolver dependency-free.
|
||||||
|
*
|
||||||
|
* @param svg SVG source bytes, expected to be UTF-8. Invalid UTF-8 is preserved
|
||||||
|
* byte-for-byte, since the resolver never decodes the document.
|
||||||
|
* @param color Concrete SVG color (for example "#ff00ff"), as ASCII bytes.
|
||||||
|
* @return The recolored SVG source; the input is left untouched.
|
||||||
|
*/
|
||||||
|
QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color);
|
||||||
|
|
||||||
|
#endif // SVG_RECOLOR_H
|
||||||
|
|
@ -177,6 +177,7 @@ cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIB
|
||||||
cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(
|
cockatrice_add_test(
|
||||||
NAME
|
NAME
|
||||||
|
|
|
||||||
66
tests/svg_recolor_test.cpp
Normal file
66
tests/svg_recolor_test.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include "libcockatrice/utility/svg_recolor.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
TEST(SvgRecolor, ReplacesEverySentinel)
|
||||||
|
{
|
||||||
|
const QByteArray svg = R"(<svg><path fill="currentColor"/><path stroke="currentColor"/></svg>)";
|
||||||
|
const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800");
|
||||||
|
|
||||||
|
EXPECT_EQ(result.count("#ff8800"), 2);
|
||||||
|
EXPECT_FALSE(result.contains("currentColor"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase)
|
||||||
|
{
|
||||||
|
const QByteArray svg = R"(<svg><path fill="CurrentColor"/><path stroke="CURRENTCOLOR"/></svg>)";
|
||||||
|
|
||||||
|
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact)
|
||||||
|
{
|
||||||
|
const QByteArray svg =
|
||||||
|
R"(<svg viewBox="0 0 24 24"><!-- an ordinary comment --><path d="M0 0h24v24" fill="currentColor"/></svg>)";
|
||||||
|
const QByteArray result = resolveSvgColorSentinel(svg, "#123456");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.contains(R"(viewBox="0 0 24 24")"));
|
||||||
|
EXPECT_TRUE(result.contains(R"(d="M0 0h24v24")"));
|
||||||
|
EXPECT_TRUE(result.contains("an ordinary comment"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SvgRecolor, DoesNotModifyTheInput)
|
||||||
|
{
|
||||||
|
const QByteArray svg = R"(<svg><path fill="currentColor"/></svg>)";
|
||||||
|
const QByteArray original = svg;
|
||||||
|
|
||||||
|
resolveSvgColorSentinel(svg, "#000000");
|
||||||
|
|
||||||
|
EXPECT_EQ(svg, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SvgRecolor, PreservesBytesThatAreNotValidUtf8)
|
||||||
|
{
|
||||||
|
QByteArray svg = "<svg>";
|
||||||
|
svg.append("\xff\xfe");
|
||||||
|
svg.append(R"(<path fill="currentColor"/></svg>)");
|
||||||
|
|
||||||
|
const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.contains("\xff\xfe"));
|
||||||
|
EXPECT_FALSE(result.contains("currentColor"));
|
||||||
|
EXPECT_EQ(result.count("#ff8800"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SvgRecolor, NoSentinelIsANoOp)
|
||||||
|
{
|
||||||
|
const QByteArray svg = R"(<svg><path fill="#abcdef"/></svg>)";
|
||||||
|
|
||||||
|
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Loading…
Add table
Add a link
Reference in a new issue