Loading...
Searching...
No Matches
AudioPluginCache.hpp
1#pragma once
2
3// Versioned persistence for the audio plug-in scan caches (VST2 / VST3 /
4// CLAP / LV2), replacing the raw QVariant metatype blobs in QSettings.
5//
6// The old scheme stored `QVariant::fromValue(std::vector<Info>)`: any change
7// to the Info datastream layout made old blobs decode as garbage that
8// `QVariant::canConvert` happily accepted (the "vst_invalid_format" global
9// hack in the VST2 plug-in was a workaround for exactly this). The new blob
10// is explicit: magic, format version, element count, elements — and
11// deserialization fails cleanly instead of producing garbage when anything
12// does not line up.
13//
14// deduplicated() / dropShadowedInvalidEntries() heal caches that already
15// accumulated duplicates (each plug-in could end up multiplied by hundreds:
16// scan replies from *other* score instances used to be appended to whichever
17// instance owned the fixed notification port, and nothing ever pruned them).
18
19#include <ossia/detail/algorithms.hpp>
20
21#include <QByteArray>
22#include <QDataStream>
23#include <QIODevice>
24#include <QSet>
25#include <QSettings>
26#include <QString>
27
28#include <optional>
29#include <type_traits>
30#include <vector>
31
32namespace Media
33{
34inline constexpr quint32 pluginCacheMagic = 0x53435043; // "SCPC"
35
52template <typename T>
53QByteArray serializePluginCache(quint32 version, const std::vector<T>& vec)
54{
55 QByteArray res;
56 {
57 QDataStream str{&res, QIODevice::WriteOnly};
58 str << pluginCacheMagic << version << (quint32)vec.size();
59 for(const auto& elt : vec)
60 {
61 QByteArray blob;
62 {
63 QDataStream es{&blob, QIODevice::WriteOnly};
64 es << elt;
65 }
66 str << blob;
67 }
68 }
69 return res;
70}
71
74template <typename T>
75std::optional<std::vector<T>>
76deserializePluginCache(quint32 version, const QByteArray& data)
77{
78 if(data.isEmpty())
79 return std::nullopt;
80
81 QDataStream str{data};
82 quint32 magic{}, ver{}, count{};
83 str >> magic >> ver >> count;
84 if(str.status() != QDataStream::Ok || magic != pluginCacheMagic || ver != version)
85 return std::nullopt;
86
87 // Each element blob costs at least its 4-byte length prefix; a larger
88 // count is a corrupt header, not a plausible plug-in collection.
89 if(count > (quint32)data.size() / 4)
90 return std::nullopt;
91
92 std::vector<T> res;
93 res.reserve(count);
94 for(quint32 i = 0; i < count; i++)
95 {
96 QByteArray blob;
97 str >> blob;
98 if(str.status() != QDataStream::Ok)
99 return std::nullopt;
100
101 QDataStream es{blob};
102 T elt;
103 es >> elt;
104 // An element that does not consume its blob exactly decoded with a
105 // different layout than it was written with
106 if(es.status() != QDataStream::Ok || !es.atEnd())
107 return std::nullopt;
108 res.push_back(std::move(elt));
109 }
110
111 if(!str.atEnd())
112 return std::nullopt;
113
114 return res;
115}
116
120template <typename T>
121std::vector<T> loadPluginCache(
122 quint32 version, const QString& key, const QString& legacyKey)
123{
124 QSettings set;
125
126 std::optional<std::vector<T>> versioned;
127 if(const auto val = set.value(key); val.canConvert<QByteArray>())
128 versioned = deserializePluginCache<T>(version, val.toByteArray());
129
130 std::vector<T> legacy;
131 if(!legacyKey.isEmpty())
132 {
133 if(!versioned)
134 {
135 if(const auto val = set.value(legacyKey); val.canConvert<std::vector<T>>())
136 legacy = val.value<std::vector<T>>();
137 }
138 // Removed even when the versioned cache won: an older score run may have
139 // rewritten it since the migration, and it must not resurrect stale
140 // entries after a future format-version bump.
141 set.remove(legacyKey);
142 }
143
144 return versioned ? *std::move(versioned) : legacy;
145}
146
147template <typename T>
148void savePluginCache(quint32 version, const QString& key, const std::vector<T>& vec)
149{
150 QSettings{}.setValue(key, serializePluginCache(version, vec));
151}
152
155template <typename T, typename KeyFn>
156void deduplicate(std::vector<T>& vec, KeyFn&& key_of)
157{
158 // `[](const Info& i) { return i.path + "|" + i.id; }` deduces a
159 // QStringBuilder return type. The outer builder holds a reference to the
160 // inner one, which is a temporary of the return statement, so the object
161 // handed back here points at freed memory and converting it reads a garbage
162 // length (std::bad_alloc, on the caller's first plug-in). Demanding QString
163 // makes the concatenation happen where its operands are still alive.
164 static_assert(
165 std::is_same_v<std::invoke_result_t<KeyFn&, const T&>, QString>,
166 "the key function must return QString: a deduced QStringBuilder return "
167 "type dangles past the end of the return statement");
168 QSet<QString> seen;
169 ossia::remove_erase_if(vec, [&](const T& elt) {
170 const QString k = key_of(elt);
171 if(seen.contains(k))
172 return true;
173 seen.insert(k);
174 return false;
175 });
176}
177
181template <typename T, typename PathFn, typename ValidFn>
182void dropShadowedInvalidEntries(std::vector<T>& vec, PathFn&& path_of, ValidFn&& is_valid)
183{
184 static_assert(
185 std::is_same_v<std::invoke_result_t<PathFn&, const T&>, QString>,
186 "the path function must return QString: see deduplicate()");
187 QSet<QString> valid_paths;
188 for(const auto& elt : vec)
189 if(is_valid(elt))
190 valid_paths.insert(path_of(elt));
191
192 ossia::remove_erase_if(vec, [&](const T& elt) {
193 return !is_valid(elt) && valid_paths.contains(path_of(elt));
194 });
195}
196
199template <typename T, typename KeyFn, typename PathFn, typename ValidFn>
200void sanitizePluginCache(
201 std::vector<T>& vec, KeyFn&& key_of, PathFn&& path_of, ValidFn&& is_valid)
202{
203 ossia::remove_erase_if(
204 vec, [&](const T& elt) { return path_of(elt).isEmpty(); });
205 dropShadowedInvalidEntries(vec, path_of, is_valid);
206 deduplicate(vec, key_of);
207}
208}