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 successfully parsed entries discovered per rescan, summed across every registered search path.
 

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 both PhosphorShaders::ShaderRegistry and PhosphorAnimationShaders::AnimationShaderRegistry 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. 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. NOT invoked when the scan is empty AND the previous scan was also empty (the no-content baseline doesn't fire on repeated empty scans).

API choice — why a class template over a virtual interface

Both real consumers (ShaderInfo in phosphor-shaders, AnimationShaderEffect in phosphor-animation-shaders) 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 either 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) the 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 exactly two of them.

Lifetime

The strategy is constructed by the consumer registry, held by reference inside 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. Both real consumers (ShaderInfo, AnimationShaderEffect) satisfy this. 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: every iterated subdir's metadata.json, 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.

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 MetadataPackRegistryBase'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 loop casts to size_t for the comparison, and a negative cast wraps to SIZE_MAX and silently disables the guard. Asserted in debug builds and clamped to zero in release so the wrap can't happen.

◆ 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 MetadataPackRegistryBase::setUserPath, which orchestrates a synchronous rescanNow() after forwarding the value, so existing pack classifications refresh immediately. 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 successfully parsed entries discovered per rescan, summed across every registered search path.

Mirrors DirectoryLoader::kMaxEntries — every fsloader-backed registry caps at the same scale. Typical pack counts are single digits; the cap is purely a DoS guard against pathological user dirs with thousands of metadata.json-bearing subdirs.

Note: the cap does not bound the number of subdirs iterated. A search path with N subdirs that all lack a valid metadata.json will still loop through all N (each one stat'd, then skipped) — the cap protects against parsed-payload blowup, not directory- walk blowup. The bounded-iteration guarantee comes from WatchedDirectorySet's forbidden-root check (which prevents $HOME and friends from being registered) and from caller-side path discipline. If a future caller registers an untrusted-tree-of-subdirs they must apply their own iteration cap.


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