From 1f5e434fe3376202f829530cda02f4f686fd3466 Mon Sep 17 00:00:00 2001 From: Simon Sobisch Date: Wed, 1 Nov 2023 07:09:08 +0000 Subject: [PATCH] peek into file before open * check file open, providing nice and early error * explicit check for perfparser (magic number QPERFSTREAM) opening directly without the need to fall back to file name * explicit check for V1 perf data which isn't supported by perfparser * explicit check for unknown file type Fixes: https://github.com/KDAB/hotspot/issues/477 --- src/parsers/perf/perfparser.cpp | 30 +++++-- tests/integrationtests/tst_perfparser.cpp | 101 ++++++++++++++++++++++ 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index f5efdd569..34e3ae73b 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -1475,6 +1475,7 @@ PerfParser::~PerfParser() = default; bool PerfParser::initParserArgs(const QString& path) { + // check for common file issues const auto info = QFileInfo(path); if (!info.exists()) { emit parsingFailed(tr("File '%1' does not exist.").arg(path)); @@ -1489,6 +1490,21 @@ bool PerfParser::initParserArgs(const QString& path) return false; } + // peek into file header + QFile file(path); + file.open(QIODevice::ReadOnly); + if (file.peek(8) != "PERFILE2" && file.peek(11) != "QPERFSTREAM") { + if (file.peek(8) == "PERFFILE") { + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, tr("Unsupported V1 perf data"))); + } else { + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, tr("File format unknown"))); + } + file.close(); + return false; + } + file.close(); + + // check perfparser and set initial values auto parserBinary = Util::perfParserBinaryPath(); if (parserBinary.isEmpty()) { emit parsingFailed(tr("Failed to find hotspot-perfparser binary.")); @@ -1583,16 +1599,16 @@ void PerfParser::startParseFile(const QString& path) emit parsingFinished(); }; - if (path.endsWith(QLatin1String(".perfparser"))) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - emit parsingFailed(tr("Failed to open file %1: %2").arg(path, file.errorString())); - return; - } + // note: file is always readable and in supported format here, + // already validated in initParserArgs() + QFile file(path); + file.open(QIODevice::ReadOnly); + if (file.peek(11) == "QPERFSTREAM") { d.setInput(&file); while (!file.atEnd() && !d.stopRequested) { if (!d.tryParse()) { - emit parsingFailed(tr("Failed to parse file")); + // TODO: provide reason + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, QStringLiteral("Unknown reason"))); return; } } diff --git a/tests/integrationtests/tst_perfparser.cpp b/tests/integrationtests/tst_perfparser.cpp index 184bff7ed..8c6e78be0 100644 --- a/tests/integrationtests/tst_perfparser.cpp +++ b/tests/integrationtests/tst_perfparser.cpp @@ -172,6 +172,107 @@ private slots: m_capabilities = host.perfCapabilities(); } + void testFileErrorHandling() + { + PerfParser parser(this); + QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed); + QString message; + + const auto notThereFile = QLatin1String("not_here"); + parser.initParserArgs(notThereFile); + + COMPARE_OR_THROW(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + // qDebug("Error message is '%s'", qPrintable(message)); + QVERIFY(message.contains(notThereFile)); + QVERIFY(message.contains(QLatin1String("does not exist"))); + + const auto parentDirs = QLatin1String("../.."); + parser.initParserArgs(parentDirs); + // note: initializing parser args reset the attached spy counter + COMPARE_OR_THROW(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(parentDirs)); + QVERIFY(message.contains(QLatin1String("is not a file"))); + + const auto noRead = QLatin1String("no_r_possible"); + const auto shell = QStandardPaths::findExecutable(QStringLiteral("bash")); + if (!shell.isEmpty()) { + QProcess::execute( + shell, {QLatin1String("-c"), QStringLiteral("rm -rf %1 && touch %1 && chmod a-r %1").arg(noRead)}); + parser.initParserArgs(noRead); + + COMPARE_OR_THROW(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(noRead)); + QVERIFY(message.contains(QLatin1String("not readable"))); + } + } + + void testFileContent() + { + PerfParser parser(this); + QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed); + QSignalSpy parsingFinishedSpy(&parser, &PerfParser::parsingFinished); + QString message; + + const auto perfData = QFINDTESTDATA("custom_cost_aggregation_testfiles/custom_cost_aggregation.perfparser"); + QVERIFY(!perfData.isEmpty() && QFile::exists(perfData)); + + parser.startParseFile(perfData); + + VERIFY_OR_THROW(parsingFinishedSpy.wait(6000)); + + // Verify that the test passed + COMPARE_OR_THROW(parsingFailedSpy.count(), 0); + COMPARE_OR_THROW(parsingFinishedSpy.count(), 1); + + const auto shell = QStandardPaths::findExecutable(QStringLiteral("bash")); + if (!shell.isEmpty()) { + const auto perfDataSomeName = QLatin1String("fruitper"); + QProcess::execute(shell, + {QLatin1String("-c"), QStringLiteral("cp -f %1 %2").arg(perfData, perfDataSomeName)}); + + parser.startParseFile(perfDataSomeName); + + VERIFY_OR_THROW(parsingFinishedSpy.wait(6000)); + + // Verify that the test passed + COMPARE_OR_THROW(parsingFailedSpy.count(), 0); + COMPARE_OR_THROW(parsingFinishedSpy.count(), 2); + } + + // check for invalid data + const auto noPerf = QFINDTESTDATA("custom_cost_aggregation_testfiles/by_cpu.txt"); + QVERIFY(!noPerf.isEmpty() && QFile::exists(noPerf)); + parser.startParseFile(noPerf); + COMPARE_OR_THROW(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(noPerf)); + QVERIFY(message.contains(QLatin1String("File format unknown"))); + + // TODO: check for PERFv1 (missing: data file) + // const auto perf1 = QFINDTESTDATA("custom_cost_aggregation_testfiles/perf.data.custom_cost_aggregation"); + // QVERIFY(!perf1.isEmpty() && QFile::exists(perf1)); + // parser.startParseFile(perf1); + // COMPARE_OR_THROW(parsingFailedSpy.count(), 1); + // message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + // QVERIFY(message.contains(perf1)); + // QVERIFY(message.contains(QLatin1String("V1 perf data"))); + + // TODO: check for PERFv2 (missing: data file) + // const auto perf2 = QFINDTESTDATA("custom_cost_aggregation_testfiles/perf.data.custom_cost_aggregation"); + // QVERIFY(!perf2.isEmpty() && QFile::exists(perf2)); + + // parser.startParseFile(perf2); + + // VERIFY_OR_THROW(parsingFinishedSpy.wait(6000)); + + // // Verify that the test passed + // COMPARE_OR_THROW(parsingFailedSpy.count(), 0); + // COMPARE_OR_THROW(parsingFinishedSpy.count(), 1); + } + void init() { m_bottomUpData = {};