Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
JsonEnvelopeValidator.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
7
8#include <QtCore/QFile>
9#include <QtCore/QFileInfo>
10#include <QtCore/QJsonDocument>
11#include <QtCore/QJsonObject>
12#include <QtCore/QLatin1String>
13#include <QtCore/QLoggingCategory>
14#include <QtCore/QString>
15
16#include <optional>
17#include <utility>
18
19namespace PhosphorFsLoader {
20
31{
32 QString name;
33 QJsonObject root;
34};
35
76inline std::optional<JsonEnvelope> validateJsonEnvelope(const QString& filePath, const QLoggingCategory& category)
77{
78 // Single source of truth for the JSON-envelope size cap is
79 // `DirectoryLoader::kMaxFileBytes`. The loader applies it first via
80 // the default sink dispatch path; this helper enforces it again
81 // because `validateJsonEnvelope` is a public free function and may
82 // be called directly (without a loader stat in front of it). One
83 // extra `QFileInfo::size()` per direct call — microscopic compared
84 // with the alternative of letting a 2 GiB blob fall through to a
85 // caller that didn't stat itself.
86 QFileInfo info(filePath);
87 if (info.exists() && info.size() > DirectoryLoader::kMaxFileBytes) {
88 qCWarning(category).nospace() << "Skipping " << filePath << ": file size " << info.size() << " exceeds limit "
90 return std::nullopt;
91 }
92
93 QFile file(filePath);
94 if (!file.open(QIODevice::ReadOnly)) {
95 qCWarning(category) << "Skipping unreadable file" << filePath << ":" << file.errorString();
96 return std::nullopt;
97 }
98
99 QJsonParseError parseError;
100 const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
101 if (parseError.error != QJsonParseError::NoError) {
102 qCWarning(category) << "Skipping malformed JSON" << filePath << ":" << parseError.errorString();
103 return std::nullopt;
104 }
105 if (!doc.isObject()) {
106 qCWarning(category) << "Skipping non-object root JSON in" << filePath;
107 return std::nullopt;
108 }
109
110 QJsonObject root = doc.object();
111
112 const QString name = root.value(QLatin1String("name")).toString();
113 if (name.isEmpty()) {
114 qCWarning(category) << "Skipping" << filePath << ": missing required 'name' field";
115 return std::nullopt;
116 }
117
118 // The filename (without extension) is the user's ergonomic handle on
119 // the entity; the inner `name` field is what actually gets
120 // registered. If the two diverge — typically because the user copied
121 // `widget.fade.json → custom.json` and forgot to rename the inner
122 // field — the result registers under the inner-name key while the
123 // file on disk suggests a different identity. Reject up front with a
124 // clear diagnostic naming both sides.
125 const QString basename = QFileInfo(filePath).completeBaseName();
126 if (name != basename) {
127 qCWarning(category).nospace() << "Skipping " << filePath << ": name '" << name << "' does not match filename '"
128 << basename << "' — rejecting to avoid silent shadowing";
129 return std::nullopt;
130 }
131
132 // Strip the bookkeeping field so the sink's schema-specific
133 // `fromJson` doesn't need to know about it (e.g. ProfileLoader's
134 // `Profile::fromJson` would otherwise leak `name` into `presetName`).
135 root.remove(QLatin1String("name"));
136
137 return JsonEnvelope{name, std::move(root)};
138}
139
140} // namespace PhosphorFsLoader
static constexpr qint64 kMaxFileBytes
Per-file size cap.
Definition DirectoryLoader.h:133
Definition DirectoryLoader.h:18
std::optional< JsonEnvelope > validateJsonEnvelope(const QString &filePath, const QLoggingCategory &category)
Validate the default envelope used by DirectoryLoader sinks.
Definition JsonEnvelopeValidator.h:76
Result of a successful envelope validation.
Definition JsonEnvelopeValidator.h:31
QString name
Definition JsonEnvelopeValidator.h:32
QJsonObject root
Definition JsonEnvelopeValidator.h:33