484 struct EntryFingerprint
486 qint64 metadataSize = 0;
487 qint64 metadataMtimeMs = 0;
501 std::vector<Entry> entries;
505 QSet<QString> seenIds;
507 QStringList desiredWatches;
509 const QLoggingCategory& log = m_loggingCat ? *m_loggingCat : detail::lcMetadataPackScan();
517 const QString canonicalUserPath = m_userPath.isEmpty() ? QString() : QFileInfo(m_userPath).canonicalFilePath();
519 bool capTripped =
false;
526 for (
auto dirIt = directoriesInScanOrder.crbegin(); dirIt != directoriesInScanOrder.crend() && !capTripped;
528 const QString& searchPath = *dirIt;
529 QDir dirObj(searchPath);
530 if (!dirObj.exists()) {
531 qCDebug(log) <<
"MetadataPackScanStrategy: search path does not exist:" << searchPath;
535 const bool isUserDir =
536 !canonicalUserPath.isEmpty() && QFileInfo(searchPath).canonicalFilePath() == canonicalUserPath;
541 desiredWatches.append(m_perDirWatch(searchPath));
544 const QStringList subdirs = dirObj.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
545 for (
const QString& subdir : subdirs) {
546 if (m_subdirSkip && m_subdirSkip(subdir)) {
552 if (entries.size() >=
static_cast<std::size_t
>(m_maxEntries)) {
557 const QString subdirPath = dirObj.filePath(subdir);
558 const QString metadataPath = subdirPath + QStringLiteral(
"/metadata.json");
564 desiredWatches.append(metadataPath);
566 const QFileInfo metadataInfo(metadataPath);
567 if (!metadataInfo.exists()) {
568 qCDebug(log) <<
"MetadataPackScanStrategy: skipping subdir, no metadata.json:" << subdirPath;
578 qCWarning(log) <<
"MetadataPackScanStrategy: skipping oversized metadata.json:" << metadataPath <<
"("
583 QFile file(metadataPath);
584 if (!file.open(QIODevice::ReadOnly)) {
585 qCWarning(log) <<
"MetadataPackScanStrategy: failed to open metadata.json:" << metadataPath;
589 QJsonParseError parseError{};
590 const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
591 if (parseError.error != QJsonParseError::NoError) {
592 qCWarning(log) <<
"MetadataPackScanStrategy: parse error in" << metadataPath <<
":"
593 << parseError.errorString();
596 if (!doc.isObject()) {
597 qCWarning(log) <<
"MetadataPackScanStrategy: non-object root in" << metadataPath;
604 std::optional<Payload> parsed = m_parser(subdirPath, doc.object(), isUserDir);
605 if (!parsed.has_value()) {
606 qCDebug(log) <<
"MetadataPackScanStrategy: parser declined" << metadataPath;
609 if (parsed->id.isEmpty()) {
610 qCWarning(log) <<
"MetadataPackScanStrategy: skipping" << metadataPath <<
": empty 'id' field";
617 if (seenIds.contains(parsed->id)) {
618 qCDebug(log) <<
"MetadataPackScanStrategy: id" << parsed->id
619 <<
"already registered from a higher-priority dir; shadowed at:" << subdirPath;
624 if (m_perEntryWatch) {
625 desiredWatches.append(m_perEntryWatch(*parsed));
636 QString
id = parsed->id;
640 EntryFingerprint{metadataInfo.size(), metadataInfo.lastModified().toMSecsSinceEpoch(), isUserDir},
641 std::move(*parsed)});
646 qCWarning(log).nospace() <<
"MetadataPackScanStrategy: reached entry cap (" << m_maxEntries
647 <<
") — later entries skipped to protect the GUI thread. Prune the watched search "
648 "paths or raise the cap.";
654 std::sort(entries.begin(), entries.end(), [](
const Entry& a,
const Entry& b) {
679 QCryptographicHash hasher(QCryptographicHash::Sha1);
680 for (
const Entry& e : entries) {
681 hasher.addData(e.id.toUtf8());
682 hasher.addData(QByteArrayView(
"|"));
683 hasher.addData(e.fp.isUser ? QByteArrayView(
"u") : QByteArrayView(
"s"));
684 hasher.addData(QByteArrayView(
"|"));
685 hasher.addData(QByteArray::number(e.fp.metadataSize));
686 hasher.addData(QByteArrayView(
"|"));
687 hasher.addData(QByteArray::number(e.fp.metadataMtimeMs));
688 hasher.addData(QByteArrayView(
"|"));
690 m_sigContrib(hasher, e.payload);
692 hasher.addData(QByteArrayView(
"\n"));
698 QStringList sortedWatches = desiredWatches;
699 sortedWatches.removeDuplicates();
700 std::sort(sortedWatches.begin(), sortedWatches.end());
701 for (
const QString& path : sortedWatches) {
702 hasher.addData(path.toUtf8());
703 hasher.addData(QByteArrayView(
"|"));
704 const QFileInfo fi(path);
706 hasher.addData(QByteArray::number(fi.size()));
707 hasher.addData(QByteArrayView(
"|"));
708 hasher.addData(QByteArray::number(fi.lastModified().toMSecsSinceEpoch()));
714 hasher.addData(QByteArrayView(
"missing"));
716 hasher.addData(QByteArrayView(
"\n"));
718 const QByteArray signature = hasher.result();
720 QHash<QString, Payload> fresh;
721 fresh.reserve(
static_cast<int>(entries.size()));
722 for (Entry& e : entries) {
723 fresh.insert(e.id, std::move(e.payload));
726 const bool isFirstScan = !m_signatureSeeded;
727 const bool changed = isFirstScan ? !fresh.isEmpty() : signature != m_lastSignature;
729 m_packs = std::move(fresh);
730 m_lastSignature = signature;
731 m_signatureSeeded =
true;
733 if (changed && m_onCommit) {
745 return sortedWatches;