Pith - whittle
#include "core/Scanner.h"
#include <QFile>
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>

namespace whittle {
	namespace {

		bool statxNoFollow(const QByteArray& path, struct statx& stx) {
			return ::statx(AT_FDCWD, path.constData(), AT_SYMLINK_NOFOLLOW,
				STATX_TYPE | STATX_MODE | STATX_SIZE |
				STATX_MTIME | STATX_NLINK, &stx) == 0;
		}

		std::int64_t mtimeNs(const struct statx& stx) {
			return std::int64_t(stx.stx_mtime.tv_sec) * 1'000'000'000LL
			+ stx.stx_mtime.tv_nsec;
		}

	} 

	QHash<QString, FileRecord> Scanner::run(const QStringList& roots,
		const QSet<QString>& excluded,
		bool isUsb, bool recursive,
		const std::atomic<bool>& cancel,
		const ProgressFn& onProgress) {
		QHash<QString, FileRecord> out;
		int folders = 0, files = 0;
		qint64 bytes = 0;
		for (const QString& root : roots) {
			if (cancel.load(std::memory_order_relaxed)) break;

			const QString base = isUsb ? root : QString();

			walk(root, base, recursive, excluded, out, folders, files, bytes,
				cancel, onProgress);
		}
		if (onProgress) onProgress(folders, files, bytes);
		return out;
	}

	void Scanner::walk(const QString& dirPath, const QString& base, bool recursive,
		const QSet<QString>& excluded,
		QHash<QString, FileRecord>& out,
		int& folders, int& files, qint64& bytes,
		const std::atomic<bool>& cancel, const ProgressFn& onProgress) {
		if (cancel.load(std::memory_order_relaxed)) return;

		const QByteArray dirEnc = QFile::encodeName(dirPath);
		DIR* d = ::opendir(dirEnc.constData());
		if (!d) return;

		if (dirPath != base) {
			struct statx stx;
			if (statxNoFollow(dirEnc, stx) && S_ISDIR(stx.stx_mode)) {
				FileRecord rec;
				rec.absPath = dirPath;
				rec.relPath = dirPath.mid(base.length() + 1);
				rec.isDir   = true;
				rec.mtimeNs = mtimeNs(stx);
				out.insert(rec.relPath, rec);
				if (++folders % 256 == 0 && onProgress) onProgress(folders, files, bytes);
			}
		}

		struct dirent* ent;
		while ((ent = ::readdir(d)) != nullptr) {
			if (cancel.load(std::memory_order_relaxed)) break;
			const char* n = ent->d_name;
			if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0')))
				continue;

			const QString    childPath = dirPath + '/' + QFile::decodeName(n);
			const QByteArray childEnc  = QFile::encodeName(childPath);

			struct statx stx;
			if (!statxNoFollow(childEnc, stx)) continue;
			const auto mode = stx.stx_mode;

			if (S_ISLNK(mode) || S_ISFIFO(mode) || S_ISSOCK(mode) ||
				S_ISCHR(mode) || S_ISBLK(mode))
				continue;

			if (S_ISDIR(mode)) {
				if (recursive && !excluded.contains(childPath))
					walk(childPath, base, recursive, excluded, out,
						folders, files, bytes, cancel, onProgress);
			} else if (S_ISREG(mode)) {
				if (stx.stx_nlink > 1) continue;
				FileRecord rec;
				rec.absPath = childPath;
				rec.relPath = childPath.mid(base.length() + 1);
				rec.size    = std::int64_t(stx.stx_size);
				rec.mtimeNs = mtimeNs(stx);
				out.insert(rec.relPath, rec);
				bytes += rec.size;
				if (++files % 256 == 0 && onProgress) onProgress(folders, files, bytes);
			}
		}
		::closedir(d);
	}

}