ZaStoGram_desktop/Telegram/SourceFiles/core/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

292 lines
8.7 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/QSet>
#include <QtCore/QStringList>
#include <cstring>
#include <memory>
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
#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
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
namespace Core::Updates {
namespace {
void SetError(QString *error, const char *text) {
if (error) {
*error = QString::fromLatin1(text);
}
}
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
constexpr auto kMaxUncompressedSize = 1024 * 1024 * 1024;
#if !defined Q_OS_WIN || defined TDESKTOP_USE_PACKAGED
struct LzmaDeleter {
void operator()(lzma_stream *stream) const {
lzma_end(stream);
}
};
#endif // !Q_OS_WIN || TDESKTOP_USE_PACKAGED
[[nodiscard]] bool SafeName(const QString &name) {
if (name.isEmpty() || QString::fromUtf8(name.toUtf8()) != name) {
return false;
}
for (const auto ch : name) {
if (ch.unicode() < 32
|| QLatin1String("\\:<>\"|?*").contains(ch)) {
return false;
}
}
for (const auto &part : name.split('/')) {
if (part.isEmpty()
|| part == QLatin1String(".")
|| part == QLatin1String("..")
|| part.endsWith('.')
|| part.endsWith(' ')) {
return false;
}
const auto stem = part.section('.', 0, 0).trimmed().toUpper();
const auto devices = { "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$" };
for (const auto device : devices) {
if (stem == QLatin1String(device)) {
return false;
}
}
if (stem.size() == 4
&& (stem.startsWith(QLatin1String("COM"))
|| stem.startsWith(QLatin1String("LPT")))
&& ((stem.back() >= QLatin1Char('1')
&& stem.back() <= QLatin1Char('9'))
|| stem.back() == QChar(0xB9)
|| stem.back() == QChar(0xB2)
|| stem.back() == QChar(0xB3))) {
return false;
}
}
return true;
}
[[nodiscard]] bool AddName(
const QString &name,
QSet<QString> &files,
QSet<QString> &directories) {
const auto key = name.normalized(QString::NormalizationForm_C).toCaseFolded();
if (files.contains(key) || directories.contains(key)) {
return false;
}
for (auto slash = key.indexOf('/'); slash >= 0
; slash = key.indexOf('/', slash + 1)) {
const auto parent = key.left(slash);
if (files.contains(parent)) {
return false;
}
directories.insert(parent);
}
files.insert(key);
return true;
}
#endif // !TDESKTOP_DISABLE_AUTOUPDATE
} // namespace
// WHY: Callers authenticate bytes before this parser reads the payload:
// [lzma props on Windows,] original size, compressed bytes.
std::optional<QByteArray> DecompressUpdatePayload(
const VerifiedUpdate &verified,
QString *error) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
#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);
const auto &payload = verified.envelope.payload;
if (payload.size() <= kHeaderSize || payload.size() > kMaxPayloadSize) {
SetError(error, "Bad compressed payload size.");
return std::nullopt;
}
const auto data = payload.constData();
const auto compressedSize = size_t(payload.size() - kHeaderSize);
auto originalSize = qint32(0);
memcpy(&originalSize, data + kPropsSize, sizeof(originalSize));
if (originalSize <= 0 || originalSize > kMaxUncompressedSize) {
SetError(error, "Bad uncompressed payload size.");
return std::nullopt;
}
auto result = QByteArray(originalSize, Qt::Uninitialized);
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED
auto resultSize = SizeT(result.size());
auto sourceSize = SizeT(compressedSize);
const auto status = LzmaUncompress(
reinterpret_cast<uchar*>(result.data()),
&resultSize,
reinterpret_cast<const uchar*>(data + kHeaderSize),
&sourceSize,
reinterpret_cast<const uchar*>(data),
LZMA_PROPS_SIZE);
if (status != SZ_OK
|| sourceSize != compressedSize
|| resultSize != size_t(originalSize)) {
SetError(error, "Could not decompress LZMA payload.");
return std::nullopt;
}
#else // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
auto stream = lzma_stream LZMA_STREAM_INIT;
const auto guard = std::unique_ptr<lzma_stream, LzmaDeleter>(&stream);
if (lzma_stream_decoder(
&stream,
kMaxUncompressedSize,
LZMA_CONCATENATED) != LZMA_OK) {
SetError(error, "Could not initialize XZ decoder.");
return std::nullopt;
}
stream.avail_in = compressedSize;
stream.next_in = reinterpret_cast<const uint8_t*>(data + kHeaderSize);
stream.avail_out = result.size();
stream.next_out = reinterpret_cast<uint8_t*>(result.data());
const auto status = lzma_code(&stream, LZMA_FINISH);
if (status != LZMA_STREAM_END || stream.avail_in || stream.avail_out) {
SetError(error, "Could not decompress complete XZ payload.");
return std::nullopt;
}
#endif // Q_OS_WIN && !TDESKTOP_USE_PACKAGED
return result;
#else // !TDESKTOP_DISABLE_AUTOUPDATE
SetError(error, "Automatic updates are disabled.");
return std::nullopt;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
bool ExtractUpdateFiles(
const QByteArray &uncompressed,
quint64 expectedVersion,
const QString &directory,
QString *error) {
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
if (uncompressed.size() > kMaxUncompressedSize) {
SetError(error, "Uncompressed payload is too large.");
return false;
}
auto stream = QDataStream(uncompressed);
stream.setVersion(QDataStream::Qt_5_1);
auto version = quint32(0);
auto count = quint32(0);
stream >> version >> count;
if (stream.status() != QDataStream::Ok
|| version != UpdateVersionBase(expectedVersion)) {
SetError(error, "Inner update version does not match envelope.");
return false;
}
#ifdef Q_OS_WIN
constexpr auto kMinimumEntrySize = 14;
#else // Q_OS_WIN
constexpr auto kMinimumEntrySize = 15;
#endif // Q_OS_WIN
const auto device = stream.device();
if (!count || count > device->bytesAvailable() / kMinimumEntrySize) {
SetError(error, "Bad update file count.");
return false;
}
auto files = QSet<QString>();
auto directories = QSet<QString>();
for (auto i = quint32(0); i != count; ++i) {
const auto namePosition = device->pos();
auto nameSize = quint32(0);
stream >> nameSize;
if (stream.status() != QDataStream::Ok
|| !nameSize
|| (nameSize % 2)
|| nameSize > device->bytesAvailable()
|| !device->seek(namePosition)) {
SetError(error, "Bad update filename size.");
return false;
}
auto name = QString();
auto fileSize = quint32(0);
auto dataSize = quint32(0);
stream >> name >> fileSize >> dataSize;
if (dataSize == quint32(0xFFFFFFFF)) {
dataSize = 0;
}
if (stream.status() != QDataStream::Ok
|| fileSize != dataSize
|| dataSize > device->bytesAvailable()) {
SetError(error, "Bad update file size.");
return false;
}
const auto data = uncompressed.constData() + device->pos();
if (!device->seek(device->pos() + dataSize)) {
SetError(error, "Truncated update file.");
return false;
}
auto executable = quint8(0);
#ifndef Q_OS_WIN
stream >> executable;
#endif // !Q_OS_WIN
if (stream.status() != QDataStream::Ok || executable > 1) {
SetError(error, "Bad update executable flag.");
return false;
}
if (!SafeName(name) || !AddName(name, files, directories)) {
SetError(error, "Unsafe or conflicting update filename.");
return false;
}
auto file = QFile(QDir(directory).filePath(name));
if (!QDir().mkpath(QFileInfo(file).absolutePath())
|| !file.open(QIODevice::WriteOnly | QIODevice::NewOnly)) {
SetError(error, "Could not create extracted update file.");
return false;
}
if (file.write(data, dataSize) != dataSize || !file.flush()) {
SetError(error, "Could not write extracted update file.");
return false;
}
if (executable && !file.setPermissions(file.permissions()
| QFileDevice::ExeOwner
| QFileDevice::ExeUser
| QFileDevice::ExeGroup
| QFileDevice::ExeOther)) {
SetError(error, "Could not set update executable permissions.");
return false;
}
file.close();
if (file.error() != QFileDevice::NoError) {
SetError(error, "Could not close extracted update file.");
return false;
}
}
if (!stream.atEnd()) {
SetError(error, "Unexpected trailing update data.");
return false;
}
return true;
#else // !TDESKTOP_DISABLE_AUTOUPDATE
SetError(error, "Automatic updates are disabled.");
return false;
#endif // TDESKTOP_DISABLE_AUTOUPDATE
}
} // namespace Core::Updates