ZaStoGram_desktop/Telegram/SourceFiles/tests/test_update_unpack.cpp
John Preston 576bf1ec10 Add a root update-install mode to the Linux binary
Task: 2026/09/29/add-a-root-update-install-mode-to-the-linux-binary
2026-10-01 14:06:56 +04:00

349 lines
12 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/update_unpack.h"
#include "core/update_verify.h"
#include <QtCore/QDataStream>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QTemporaryDir>
#include <cstring>
#include <iostream>
#include <vector>
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED
#include <LzmaLib.h>
#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
#include <lzma.h>
#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
namespace {
using namespace Core::Updates;
constexpr auto kVersion = quint32(7002009);
constexpr auto kSignedVersion = MakeUpdateVersion(kVersion, 0);
int FailedChecks = 0;
int TotalChecks = 0;
int TotalCases = 0;
struct Entry {
QString name;
QByteArray bytes;
bool executable = false;
};
void Check(bool condition, const char *name) {
++TotalChecks;
if (!condition) {
++FailedChecks;
std::cout << "FAILED: " << name << std::endl;
}
}
void Case(const char *name) {
++TotalCases;
std::cout << "CASE: " << name << std::endl;
}
[[nodiscard]] QByteArray Serialize(const std::vector<Entry> &entries) {
auto result = QByteArray();
auto stream = QDataStream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream << kVersion << quint32(entries.size());
for (const auto &entry : entries) {
stream << entry.name << quint32(entry.bytes.size()) << entry.bytes;
#ifndef Q_OS_WIN
stream << entry.executable;
#endif // !Q_OS_WIN
}
Check(stream.status() == QDataStream::Ok, "fixture serialization");
return result;
}
[[nodiscard]] QByteArray Compress(const QByteArray &input) {
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED
constexpr auto kPropsSize = LZMA_PROPS_SIZE;
#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
constexpr auto kPropsSize = 0;
#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
constexpr auto kHeaderSize = kPropsSize + sizeof(qint32);
auto result = QByteArray(input.size() * 2 + 1024, Qt::Uninitialized);
const auto originalSize = qint32(input.size());
memcpy(result.data() + kPropsSize, &originalSize, sizeof(originalSize));
auto compressedSize = size_t(result.size() - kHeaderSize);
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED
auto propsSize = size_t(kPropsSize);
const auto status = LzmaCompress(
reinterpret_cast<uchar*>(result.data() + kHeaderSize),
&compressedSize,
reinterpret_cast<const uchar*>(input.constData()),
input.size(),
reinterpret_cast<uchar*>(result.data()),
&propsSize,
5,
0,
-1,
-1,
-1,
-1,
1);
Check(status == SZ_OK, "fixture LZMA compression");
#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
auto position = size_t(0);
const auto status = lzma_easy_buffer_encode(
6,
LZMA_CHECK_CRC64,
nullptr,
reinterpret_cast<const uint8_t*>(input.constData()),
input.size(),
reinterpret_cast<uint8_t*>(result.data() + kHeaderSize),
&position,
compressedSize);
Check(status == LZMA_OK, "fixture XZ compression");
compressedSize = position;
#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
result.resize(kHeaderSize + compressedSize);
return result;
}
[[nodiscard]] QByteArray Read(const QString &path) {
auto file = QFile(path);
Check(file.open(QIODevice::ReadOnly), "open extracted file or sentinel");
return file.readAll();
}
[[nodiscard]] QString NewDirectory(const QString &parent) {
static auto serial = 0;
const auto result = parent + '/' + QString::number(++serial);
Check(QDir().mkdir(result), "fresh extraction directory");
return result;
}
void Reject(
const QByteArray &payload,
const QString &parent,
const QString &sentinel,
const char *label) {
Case(label);
auto error = QString();
Check(!ExtractUpdateFiles(
payload,
kSignedVersion,
NewDirectory(parent),
&error), "malformed payload rejected");
Check(!error.isEmpty(), "rejection provides an error");
Check(Read(sentinel) == "outside unchanged", "external sentinel unchanged");
}
void ReplaceWord(QByteArray &data, qsizetype offset, quint32 value) {
auto bytes = QByteArray();
auto stream = QDataStream(&bytes, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream << value;
data.replace(offset, bytes.size(), bytes);
}
void TestFiles(const QString &parent, const QString &sentinel) {
Case("valid multiple files, empty file, nested paths and executable modes");
const auto entries = std::vector<Entry>{
{ QString::fromLatin1("Telegram"), "main program", true },
{ QString::fromLatin1("Updater"), "helper program", true },
{ QString::fromLatin1("resources/a file"), QByteArray("a\0b", 3) },
{ QString::fromLatin1("resources/empty"), QByteArray() },
};
const auto payload = Serialize(entries);
const auto directory = NewDirectory(parent);
auto verified = VerifiedUpdate();
verified.envelope.version = kSignedVersion;
verified.envelope.payload = Compress(payload);
auto error = QString();
const auto decoded = DecompressUpdatePayload(verified, &error);
Check(decoded && *decoded == payload, "exact decompressed fixture bytes");
Check(decoded && ExtractUpdateFiles(
*decoded,
kSignedVersion,
directory,
&error), "valid fixture extracts");
for (const auto &entry : entries) {
const auto path = directory + '/' + entry.name;
Check(Read(path) == entry.bytes, "extracted bytes match independent fixture");
#ifndef Q_OS_WIN
const auto permissions = QFileInfo(path).permissions();
const auto executable = QFileDevice::ExeOwner
| QFileDevice::ExeGroup
| QFileDevice::ExeOther;
Check((permissions & executable) == (entry.executable
? executable
: QFileDevice::Permissions()), "all executable mode bits match");
#endif // !Q_OS_WIN
}
Check(!QFileInfo::exists(directory + QLatin1String("/ready")),
"extractor does not emit ready marker");
Check(!QFileInfo::exists(directory + QLatin1String("/tdata/version")),
"extractor does not emit version marker");
auto changed = payload;
ReplaceWord(changed, 0, kVersion + 1);
Reject(changed, parent, sentinel, "inner version mismatch");
changed = payload;
ReplaceWord(changed, 4, 0);
Reject(changed, parent, sentinel, "empty file count");
changed = payload;
ReplaceWord(changed, 4, 0xFFFFFFFF);
Reject(changed, parent, sentinel, "absurd file count");
changed = payload;
ReplaceWord(changed, 8, 0xFFFFFFFE);
Reject(changed, parent, sentinel, "oversized filename allocation");
changed = payload;
ReplaceWord(changed, 8, 3);
Reject(changed, parent, sentinel, "odd UTF-16 filename size");
changed = payload;
const auto sizeOffset = 12 + entries.front().name.size() * 2;
ReplaceWord(changed, sizeOffset, 0xFFFFFFFE);
Reject(changed, parent, sentinel, "inconsistent declared file size");
changed = payload;
ReplaceWord(changed, sizeOffset, 0xFFFFFFFE);
ReplaceWord(changed, sizeOffset + 4, 0xFFFFFFFE);
Reject(changed, parent, sentinel, "oversized byte array allocation");
Reject(payload.first(3), parent, sentinel, "truncated header");
Reject(payload.first(12), parent, sentinel, "truncated entry metadata");
Reject(payload.chopped(1), parent, sentinel, "truncated final entry");
Reject(payload + 'x', parent, sentinel, "trailing payload bytes");
#ifndef Q_OS_WIN
changed = payload;
changed[sizeOffset + 8 + entries.front().bytes.size()] = char(2);
Reject(changed, parent, sentinel, "invalid executable flag");
#endif // !Q_OS_WIN
const auto names = std::vector<QString>{
QString(),
QString::fromLatin1("/absolute"),
sentinel,
QString::fromLatin1("../sentinel"),
QString::fromLatin1("nested/../../sentinel"),
QString::fromLatin1("./file"),
QString::fromLatin1("nested/./file"),
QString::fromLatin1("nested/../file"),
QString::fromLatin1("nested//file"),
QString::fromLatin1("nested/"),
QString::fromLatin1("C:/file"),
QString::fromLatin1("C:file"),
QString::fromLatin1("//server/share/file"),
QString::fromLatin1("\\\\server\\share\\file"),
QString::fromLatin1("nested\\file"),
QString::fromLatin1("file:stream"),
QString::fromLatin1("file."),
QString::fromLatin1("file "),
QString::fromLatin1("NUL.txt"),
QString::fromLatin1("COM1"),
QString::fromLatin1("CON .txt"),
QString::fromLatin1("CONOUT$"),
QString::fromUtf8("COM\xC2\xB9"),
QString::fromLatin1("file\0alias", 10),
QString(QChar(0xD800)),
};
for (const auto &name : names) {
Reject(Serialize({ { name, "unsafe" } }),
parent,
sentinel,
"unsafe relative name");
}
const auto pairs = std::vector<std::pair<QString, QString>>{
{ QString::fromLatin1("same"), QString::fromLatin1("same") },
{ QString::fromLatin1("Name"), QString::fromLatin1("name") },
{ QString::fromLatin1("file"), QString::fromLatin1("file/child") },
{ QString::fromLatin1("dir/child"), QString::fromLatin1("dir") },
{ QString::fromLatin1("Dir/child"), QString::fromLatin1("dir") },
{ QString::fromUtf8("\xC3\xA9"), QString::fromUtf8("e\xCC\x81") },
};
for (const auto &[first, second] : pairs) {
const auto duplicateDirectory = NewDirectory(parent);
Case("duplicate or conflicting file and directory names");
Check(!ExtractUpdateFiles(Serialize({
{ first, "first" },
{ second, "second" },
}), kSignedVersion, duplicateDirectory), "conflict rejected");
Check(Read(duplicateDirectory + '/' + first) == "first",
"conflict did not overwrite the first file");
Check(Read(sentinel) == "outside unchanged", "external sentinel unchanged");
}
Case("existing output is never overwritten");
Check(!ExtractUpdateFiles(payload, kSignedVersion, directory),
"nonempty destination rejected");
Check(Read(directory + QLatin1String("/Telegram")) == "main program",
"existing output is unchanged");
Case("destination parent is a file");
Check(!ExtractUpdateFiles(payload, kSignedVersion, sentinel),
"mkdir failure returns failure");
Check(Read(sentinel) == "outside unchanged", "external sentinel unchanged");
}
void TestCompression() {
const auto plain = Serialize({ { QString::fromLatin1("file"), "bytes" } });
const auto compressed = Compress(plain);
const auto reject = [](QByteArray payload, const char *label) {
Case(label);
auto verified = VerifiedUpdate();
verified.envelope.payload = std::move(payload);
auto error = QString();
Check(!DecompressUpdatePayload(verified, &error),
"invalid compressed payload rejected");
Check(!error.isEmpty(), "decompression error provided");
};
reject(QByteArray(), "empty compressed payload");
reject(compressed.first(3), "truncated compression header");
reject(compressed.chopped(8), "truncated compressed bytes");
reject(compressed + 'x', "trailing compressed bytes");
auto corrupt = compressed;
corrupt[corrupt.size() / 2] ^= char(0x80);
reject(corrupt, "corrupt compressed bytes");
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED
constexpr auto kSizeOffset = LZMA_PROPS_SIZE;
#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
constexpr auto kSizeOffset = 0;
#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
for (const auto size : { qint32(0), qint32(-1), qint32(1024 * 1024 * 1024 + 1),
qint32(plain.size() - 1), qint32(plain.size() + 1) }) {
auto payload = compressed;
memcpy(payload.data() + kSizeOffset, &size, sizeof(size));
reject(payload, "invalid declared decompressed size");
}
Case("repeated decoder failure cleanup");
auto verified = VerifiedUpdate();
verified.envelope.payload = compressed.chopped(1);
for (auto i = 0; i != 200; ++i) {
Check(!DecompressUpdatePayload(verified), "repeated failed decoder");
}
}
} // namespace
int main() {
auto temporary = QTemporaryDir();
Check(temporary.isValid(), "temporary directory created");
if (!temporary.isValid()) {
return 1;
}
const auto sentinel = temporary.path() + QLatin1String("/sentinel");
{
auto file = QFile(sentinel);
Check(file.open(QIODevice::WriteOnly), "sentinel created");
Check(file.write("outside unchanged") == 17, "sentinel written");
Check(file.flush(), "sentinel flushed");
}
TestFiles(temporary.path(), sentinel);
TestCompression();
std::cout << "Cases: " << TotalCases << ", checks: " << TotalChecks
<< ", failures: " << FailedChecks << std::endl;
return FailedChecks ? 1 : 0;
}