Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
PhosphorFsLoader::MetadataPackScanStrategy< Payload > Class Template Reference

Reusable scan strategy for metadata.json-driven subdirectory pack registries. More...

#include <phosphor-fsloader/include/PhosphorFsLoader/MetadataPackScanStrategy.h>

Inheritance diagram for PhosphorFsLoader::MetadataPackScanStrategy< Payload >:
[legend]

Public Types

using Parser = std::function< std::optional< Payload >(const QString &subdirPath, const QJsonObject &root, bool isUser)>
 Parse one metadata.json into a payload.
 
using PerEntryWatchPaths = std::function< QStringList(const Payload &)>
 Extract the per-payload paths the base must re-arm individual file watches on after every rescan.
 
using PerDirectoryWatchPaths = std::function< QStringList(const QString &searchPath)>
 Optional: per-search-path watch additions beyond per-pack extraction.
 
using PerSubdirSkip = std::function< bool(const QString &subdirName)>
 Optional: predicate skipping subdirectories whose bare name matches a sentinel.
 
using SignatureContrib = std::function< void(QCryptographicHash &, const Payload &)>
 Optional: payload-specific bytes to fan into the per-rescan SHA-1 signature.
 
using OnCommit = std::function< void()>
 Synchronous "the discovered set changed" hook.
 

Public Member Functions

 MetadataPackScanStrategy (Parser parser, OnCommit onCommit)
 Construct with the parser + commit hook (the two always-required policies).
 
 ~MetadataPackScanStrategy () override=default
 
 MetadataPackScanStrategy (const MetadataPackScanStrategy &)=delete
 
MetadataPackScanStrategyoperator= (const MetadataPackScanStrategy &)=delete
 
void setPerEntryWatchPaths (PerEntryWatchPaths fn)
 Set the per-payload watch-extractor. Default: empty list.
 
void setPerDirectoryWatchPaths (PerDirectoryWatchPaths fn)
 Set the per-directory watch-extractor. Default: empty list.
 
void setPerSubdirSkip (PerSubdirSkip fn)
 Set the per-subdir-name skip predicate. Default: never skip.
 
void setSignatureContrib (SignatureContrib fn)
 Set the payload-specific signature contributor. Default: contributes nothing.
 
void setUserPath (const QString &path)
 Set the user-data search path used for isUser classification.
 
void setMaxEntries (int cap)
 Per-rescan entry cap.
 
void setLoggingCategory (const QLoggingCategory &cat)
 Override the logging category used for the strategy's own warnings (cap trip, oversized metadata.json, parse error, missing id).
 
QStringList performScan (const QStringList &directoriesInScanOrder) override
 Run a full rescan across directoriesInScanOrder.
 
const QHash< QString, Payload > & packsById () const
 Live entries by id.
 
QList< Payload > packs () const
 Live entries sorted by id for deterministic ordering.
 
QStringList packIds () const
 Live entry ids in lexicographic order.
 
Payload pack (const QString &id) const
 Lookup by id.
 
bool contains (const QString &id) const
 True if id is registered.
 
int size () const
 Number of currently registered packs.
 
- Public Member Functions inherited from PhosphorFsLoader::IScanStrategy
virtual ~IScanStrategy ()=default
 
 IScanStrategy (const IScanStrategy &)=delete
 
IScanStrategyoperator= (const IScanStrategy &)=delete
 

Static Public Attributes

static constexpr int kDefaultMaxEntries = 10'000
 Hard cap on subdirs CONSIDERED per rescan, summed across every registered search path — not on the entries that survive to registration.
 

Additional Inherited Members

- Protected Member Functions inherited from PhosphorFsLoader::IScanStrategy
 IScanStrategy ()=default
 

Detailed Description

template<typename Payload>
class PhosphorFsLoader::MetadataPackScanStrategy< Payload >

Reusable scan strategy for metadata.json-driven subdirectory pack registries.

Owns the cross-cutting scaffolding all three pack registries (PhosphorShaders::ShaderRegistry, PhosphorAnimationShaders::AnimationShaderRegistry and PhosphorSurfaceShaders::SurfaceShaderRegistry) were duplicating verbatim:

  1. Reverse-iterate directoriesInScanOrder (the canonical [lowest-priority, ..., highest-priority] shape from WatchedDirectorySet); first-registration-wins on id collision yields the XDG semantic user > sys-highest > ... > sys-lowest.
  2. Walk every subdirectory under each registered search path, build the metadata.json path, and stat-cap the file at DirectoryLoader::kMaxFileBytes before reading (untrusted same-user blob DoS guard).
  3. Parse metadata.json into a caller-supplied schema-specific Payload via the Parser policy. Parser-returned std::nullopt skips the entry; an empty Payload::id skips it as well.
  4. Per-rescan entry cap (caller-tunable, default 10,000) — when tripped, system overflow is dropped (reverse-iteration scans user dirs first, so cap-trip never violates user-wins layering).
  5. SHA-1 signature over the sorted-by-id entry set. The strategy contributes id + isUser + metadata.json size+mtime per entry, AND mixes path|size|mtime per distinct watched file (the metadata.json itself, plus everything PerEntryWatchPaths and PerDirectoryWatchPaths returned) in lex-sorted order. Mixing the path string is load-bearing: a file moving from a sys-dir to the user-dir without any byte change must still register as a layering shift, since the isUser classification on the winning entry has flipped. The "watch list = signature scope" invariant means: any file the strategy is watching contributes to the signature, so any file change the watcher fires on translates to an OnCommit (modulo mtime/size shifting, which POSIX guarantees on content-changing writes). Without this auto-mix, an edit to a watched file the caller's SignatureContrib doesn't enumerate (a top-level shared *.glsl include, a per-pack helpers.glsl, etc.) would fire the rescan but leave the signature stable — OnCommit would be silenced and consumers would hold stale state. OnCommit fires only when the signature differs from the previous scan, giving the consumer change-only emit semantics on top of WatchedDirectorySet's unconditional rescanCompleted.

    One known wrinkle: metadata.json files that fail validation (parse error, oversized, empty id, parser declined) are still added to the watch set so a fix-up edit re-fires the rescan. So is the DIRECTORY of a pack that has no metadata.json yet, and a directory's mtime moves whenever any child appears — so dropping an unrelated file into a metadata-less pack dir also shifts the fingerprint. That means an in-place edit of a still-broken file shifts the watch-set fingerprint and trips OnCommit even though no visible state changed. Consumers gating their public signal on OnCommit will emit a spurious "changed" event in that case; availableShaders() / availableEffects() reflect the truth — the spurious wakeup is harmless beyond a redundant QML rebind. A subdir-walk that's bounded only by the filesystem (see kDefaultMaxEntries) makes pinning this stricter not worth the loop bookkeeping.

  6. Sorted-by-id accessor (packs()) so QHash randomisation never surfaces in downstream UI / snapshot tests.
  7. isUser classification per entry, derived from a caller-supplied user-path and per-iterated-dir canonicalisation.

Schema-specific concerns are delegated to caller-supplied policy callables (held by value as std::function members; see "Lifetime" below):

  • Parser — turns one (subdirPath, jsonRoot, isUser) into an optional Payload. The strategy DOES NOT prescribe which JSON fields the parser consumes; mutating the payload's id, applying directory-relative path resolution, and any inline validation (e.g. multipass-needs-buffer-shader) all happen here.
  • PerEntryWatchPaths — extracts the per-payload watch paths (frag/vert/kwin/include shaders, additional metadata) the WatchedDirectorySet base must re-arm individual file watches on after every rescan. The strategy itself always watches the metadata.json; the callback adds payload-internal paths.
  • PerDirectoryWatchPaths (optional) — additional paths to watch per registered search path, NOT per pack. Used by the shader-pack registry to watch top-level shared *.glsl includes.
  • PerSubdirSkip (optional) — predicate over the bare subdirectory name. Used by the shader-pack registry to reserve none as a "no shader" sentinel.
  • SignatureContrib (optional) — fans the payload-specific fingerprint bytes into the running SHA-1. The strategy itself already contributes id + isUser + the metadata.json's size+mtime per entry, so any change to a parser-consumed metadata field surfaces as a signature shift without the contributor having to enumerate every field. Callers add what's OUTSIDE metadata.json and still affects rendering (frag-shader mtime+size, vertex-shader path, source-dir absolute path, etc.).
  • OnCommit — invoked synchronously inside performScan after the fresh map has replaced the prior one, AND ONLY WHEN the SHA-1 signature differs. The consumer wires its content-changed signal in here. changed is isFirstScan ? !fresh.isEmpty() : signature != m_lastSignature, so: a FIRST scan that found nothing never fires (the no-content baseline, and note this comes from the isFirstScan branch, not from signature equality — the SHA-1 of nothing does differ from the empty seed); a later scan fires whenever the signature moved, including one that drops to an empty result, and including one whose result set is unchanged but whose WATCH set moved — see the known wrinkle above.

API choice — why a class template over a virtual interface

The real consumers (ShaderPack, AnimationPack and SurfacePack, each hosted by its registry through PhosphorRegistry::MetadataPackLoader) parse into concrete struct types they already own. A virtual IMetadataPackPayload interface would force each registry to box its parse output (heap allocation per pack) and hide its public payload type behind a base — making packs() / pack(id) lookup callers downcast on every access. A class template lets each registry keep its concrete payload type and share the per-rescan map via direct strongly-typed accessors. The template is also header-only, which keeps phosphor-fsloader itself free of dependencies on any consumer's payload schema.

The policy callables themselves are stored as std::function (one level of type-erasure indirection per call). Inlining-via-callable- type-template-params would shave that, but at this workload (one stat syscall + JSON parse per entry per rescan) that indirection cost is unmeasurable — keeping the policy types out of the template signature is the better readability tradeoff. The price is one set of compiler instantiations per consumer payload — negligible given there are three of them.

Lifetime

The strategy is constructed and OWNED by the hosting PhosphorRegistry::MetadataPackLoader (via unique_ptr), and borrowed by reference by its WatchedDirectorySet, and destroyed when the registry is. Parser / watch-extractor callables are stored by value (typically std::function); they may capture state by pointer/reference if the captured state outlives the registry.

Setter timing

All setX methods on this class (setMaxEntries, setUserPath, setLoggingCategory, setPerEntryWatchPaths, etc.) take effect on the next rescan. Calling them from inside a Parser / SignatureContrib / OnCommit callback (i.e. while a performScan is on the stack) is undefined — the running scan reads its own snapshot of the config at the top, and a mid-scan setter call would either be ignored or interleave inconsistently. Configure the strategy once during ctor / immediately after, then leave it alone.

Thread safety

GUI-thread only. Inherits WatchedDirectorySet's threading constraint — performScan runs on the same thread the registry was constructed on.

Template Parameters
PayloadPOD-ish struct exposing a public QString id member. Must be default-constructible and movable. Every real consumer satisfies this: in production the argument is PhosphorRegistry::MetadataPackLoader<T>::Entry, wrapping ShaderPack, AnimationPack or SurfacePack. Bespoke payloads without an id field can wrap their data in a thin POD that adds one.

Member Typedef Documentation

◆ OnCommit

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::OnCommit = std::function<void()>

Synchronous "the discovered set changed" hook.

Invoked from inside performScan after the fresh map replaces the previous one. The consumer wires its public content-changed signal emission in here. The strategy guarantees the signature has actually changed before invoking; consumers don't double-diff.

◆ Parser

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::Parser = std::function<std::optional<Payload>(const QString& subdirPath, const QJsonObject& root, bool isUser)>

Parse one metadata.json into a payload.

The strategy already validated (a) the file exists, (b) it is under DirectoryLoader::kMaxFileBytes, (c) parsing succeeded and the root is a JSON object, before invoking the parser. Returning std::nullopt skips the entry silently; the strategy will warn at the call site below where appropriate.

The parser is responsible for resolving directory-relative paths inside root against subdirPath, applying any inline validation, and stamping the payload's isUser flag from isUser if the schema exposes one. Returned payloads with an empty id are dropped by the caller (warning logged).

◆ PerDirectoryWatchPaths

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::PerDirectoryWatchPaths = std::function<QStringList(const QString& searchPath)>

Optional: per-search-path watch additions beyond per-pack extraction.

Use for shared top-level files inside the search path (e.g. common.glsl in the shader-pack registry). Default: returns an empty list.

◆ PerEntryWatchPaths

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::PerEntryWatchPaths = std::function<QStringList(const Payload&)>

Extract the per-payload paths the base must re-arm individual file watches on after every rescan.

The strategy always adds the metadata.json itself; this callback is for everything else (frag / vert / kwin shaders, additional metadata, etc.).

◆ PerSubdirSkip

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::PerSubdirSkip = std::function<bool(const QString& subdirName)>

Optional: predicate skipping subdirectories whose bare name matches a sentinel.

Default: never skip. The shader-pack registry uses this to reserve none for "no shader".

◆ SignatureContrib

template<typename Payload >
using PhosphorFsLoader::MetadataPackScanStrategy< Payload >::SignatureContrib = std::function<void(QCryptographicHash&, const Payload&)>

Optional: payload-specific bytes to fan into the per-rescan SHA-1 signature.

The strategy already mixes in id, isUser, the metadata.json's size+mtime per entry, AND the size+mtime of every file PerEntryWatchPaths / PerDirectoryWatchPaths returned — so anything reachable via the watch list is already covered without enumeration. Use SignatureContrib only for non-file payload bytes that aren't represented in the watch set (e.g. a hash of an embedded resource). Default: contributes nothing.

Constructor & Destructor Documentation

◆ MetadataPackScanStrategy() [1/2]

template<typename Payload >
PhosphorFsLoader::MetadataPackScanStrategy< Payload >::MetadataPackScanStrategy ( Parser  parser,
OnCommit  onCommit 
)
inline

Construct with the parser + commit hook (the two always-required policies).

Optional policies default to no-ops via the setter API below.

Parameters
parserSchema-specific metadata.jsonPayload parser.
onCommitCalled only when the per-rescan signature differs from the previous scan (empty parser results compared to a previous empty scan are not commits).

◆ ~MetadataPackScanStrategy()

template<typename Payload >
PhosphorFsLoader::MetadataPackScanStrategy< Payload >::~MetadataPackScanStrategy ( )
overridedefault

◆ MetadataPackScanStrategy() [2/2]

template<typename Payload >
PhosphorFsLoader::MetadataPackScanStrategy< Payload >::MetadataPackScanStrategy ( const MetadataPackScanStrategy< Payload > &  )
delete

Member Function Documentation

◆ contains()

template<typename Payload >
bool PhosphorFsLoader::MetadataPackScanStrategy< Payload >::contains ( const QString &  id) const
inline

True if id is registered.

◆ operator=()

template<typename Payload >
MetadataPackScanStrategy & PhosphorFsLoader::MetadataPackScanStrategy< Payload >::operator= ( const MetadataPackScanStrategy< Payload > &  )
delete

◆ pack()

template<typename Payload >
Payload PhosphorFsLoader::MetadataPackScanStrategy< Payload >::pack ( const QString &  id) const
inline

Lookup by id.

Returns a default-constructed Payload if the id is not registered (consistent with QHash::value).

◆ packIds()

template<typename Payload >
QStringList PhosphorFsLoader::MetadataPackScanStrategy< Payload >::packIds ( ) const
inline

Live entry ids in lexicographic order.

Cheaper than packs() when the consumer only needs the keys (no per-payload copy); the strategy already maintains the canonical sort, so consumers should call this rather than packsById().keys() + std::sort.

◆ packs()

template<typename Payload >
QList< Payload > PhosphorFsLoader::MetadataPackScanStrategy< Payload >::packs ( ) const
inline

Live entries sorted by id for deterministic ordering.

QHash iteration order is randomised in Qt6; consumers exposing pack lists to UI / tests should use this rather than packsById().

◆ packsById()

template<typename Payload >
const QHash< QString, Payload > & PhosphorFsLoader::MetadataPackScanStrategy< Payload >::packsById ( ) const
inline

Live entries by id.

◆ performScan()

template<typename Payload >
QStringList PhosphorFsLoader::MetadataPackScanStrategy< Payload >::performScan ( const QStringList &  directoriesInScanOrder)
overridevirtual

Run a full rescan across directoriesInScanOrder.

See IScanStrategy::performScan for the canonical input shape. The strategy always rebuilds its full pack map from scratch — no incremental update. Stale entries (subdirs whose metadata.json vanished since the last scan) drop out by being absent from the rebuilt map; the next signature comparison reports the change and OnCommit fires.

Returns
Per-rescan watch paths: each accepted subdir's metadata.json, the SUBDIRECTORY itself for any subdir that has no metadata.json yet (the file cannot be watched before it exists, so the directory stands in for it — a subdir rejected by PerSubdirSkip is charged to the cap but contributes no watch), plus everything PerEntryWatchPaths and PerDirectoryWatchPaths returned. Lex-sorted and deduplicated (the same canonical form already used internally for the signature pass) so callers can rely on stable ordering across rescans regardless of which search-path / subdir-traversal interleaving produced each entry. Does NOT include the registered search paths themselves — the base already watches those directly.

Subdirs CONSIDERED this rescan, summed across every search path. The cap counts these rather than the entries that survive to registration: a subdir whose metadata.json is missing, oversize, unopenable, unparseable, non-object, or id-colliding has still cost a stat (and for everything past the existence check an open, a readAll and a JSON parse) on the GUI thread, which is the work the cap exists to bound. Counting registrations instead let a spray of broken packs through the guard untouched — the exact attack it names. Mirrors JsonScanStrategy's filesConsidered.

Implements PhosphorFsLoader::IScanStrategy.

◆ setLoggingCategory()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setLoggingCategory ( const QLoggingCategory &  cat)
inline

Override the logging category used for the strategy's own warnings (cap trip, oversized metadata.json, parse error, missing id).

Defaults to an internal phosphorfsloader.metadatapackscan category — the override exists so each consumer's log output keeps its per-registry filterability.

Stored as a borrowed pointer; cat must outlive the strategy. The standard caller pattern — setLoggingCategory(lcMyRegistry()) where lcMyRegistry is defined via Q_LOGGING_CATEGORY — gives a static-lifetime category and trivially satisfies this. Reference (not pointer) parameter mirrors MetadataPackLoader's ctor and makes "always non-null" a compile-time guarantee.

◆ setMaxEntries()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setMaxEntries ( int  cap)
inline

Per-rescan entry cap.

Default: kDefaultMaxEntries.

Negative values are a programming error. The cap comparison is subdirsConsidered >= m_maxEntries with both sides int, so a negative cap does not wrap — it trips on the very first subdir of every scan, which purges every previously registered pack. Asserted in debug builds and, in release, ignored in favour of kDefaultMaxEntries so a bad cap costs the caller its setting rather than the registry its contents.

Zero is a DIFFERENT case and is honoured, matching DirectoryLoader::JsonScanStrategy::setMaxEntries: "admit nothing" is a coherent thing for a caller to ask for, where "admit fewer than nothing" is a bug. The two sibling scanners must not diverge on this.

◆ setPerDirectoryWatchPaths()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setPerDirectoryWatchPaths ( PerDirectoryWatchPaths  fn)
inline

Set the per-directory watch-extractor. Default: empty list.

◆ setPerEntryWatchPaths()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setPerEntryWatchPaths ( PerEntryWatchPaths  fn)
inline

Set the per-payload watch-extractor. Default: empty list.

◆ setPerSubdirSkip()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setPerSubdirSkip ( PerSubdirSkip  fn)
inline

Set the per-subdir-name skip predicate. Default: never skip.

◆ setSignatureContrib()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setSignatureContrib ( SignatureContrib  fn)
inline

Set the payload-specific signature contributor. Default: contributes nothing.

◆ setUserPath()

template<typename Payload >
void PhosphorFsLoader::MetadataPackScanStrategy< Payload >::setUserPath ( const QString &  path)
inline

Set the user-data search path used for isUser classification.

The strategy canonicalises this via QFileInfo::canonicalFilePath once per rescan and compares each iterated dir's canonical form against it. Empty (the default) yields false for every entry's isUser. The path does not need to exist at the time of the call — canonicalFilePath resolves once it materialises and subsequent rescans pick up the classification.

Setting this directly on the strategy does not trigger a rescan — the new value takes effect on the next scan, whenever that fires. Production consumers reach the strategy through PhosphorRegistry::MetadataPackLoader::setUserPath, which forwards the value and then runs a synchronous rescanNow() — but only when the value actually changed AND at least one search path is registered, so setting the user path before any registration does not rescan either. Direct strategy access (e.g. tests using static_cast<ScanStrategy*>(strategy())) bypasses that orchestration; pair the call with an explicit WatchedDirectorySet::rescanNow() if immediate reclassification is required.

◆ size()

template<typename Payload >
int PhosphorFsLoader::MetadataPackScanStrategy< Payload >::size ( ) const
inline

Number of currently registered packs.

Member Data Documentation

◆ kDefaultMaxEntries

template<typename Payload >
constexpr int PhosphorFsLoader::MetadataPackScanStrategy< Payload >::kDefaultMaxEntries = 10'000
staticconstexpr

Hard cap on subdirs CONSIDERED per rescan, summed across every registered search path — not on the entries that survive to registration.

Mirrors DirectoryLoader::kMaxEntries, which counts the same way; every fsloader-backed registry caps at the same scale. Typical pack counts are single digits, so the cap is purely a DoS guard against pathological user dirs.

Counting registrations instead would have missed the whole attack: a subdir whose metadata.json is missing, oversize, unparseable or id-colliding registers nothing, so a spray of thousands of broken packs never advanced the counter while still costing a stat, an open, a readAll and a parse EACH, on the GUI thread, on every watcher fire — and growing the returned watch set without bound along the way.

What it still does not bound is the entryList call that materialises and sorts the subdir names before the loop begins. WatchedDirectorySet's forbidden-root check (which keeps $HOME and friends from being registered) plus caller-side path discipline are what keep that input small. A future caller registering an untrusted tree of subdirs needs its own enumeration cap.


The documentation for this class was generated from the following file: