mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
d4ac13dbf4
Revert needed, since otherwise there is no way to do automatic sorting of includes. Also reverted change to the docs, as leaving it would make incorrect docs. In case of conflicts, includes were sorted according to the coding standards from #3839. This reverts commitb4a9f04f92
. This reverts commit5921122960
.
636 lines
17 KiB
C++
636 lines
17 KiB
C++
/*
|
|
Copyright © 2015 by The qTox Project Contributors
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
qTox is libre software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
qTox is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "settingsserializer.h"
|
|
#include "serialize.h"
|
|
#include "src/nexus.h"
|
|
#include "src/persistence/profile.h"
|
|
#include "src/core/core.h"
|
|
#include <QSaveFile>
|
|
#include <QFile>
|
|
#include <QDebug>
|
|
#include <memory>
|
|
#include <cassert>
|
|
|
|
/**
|
|
* @class SettingsSerializer
|
|
* @brief Serializes a QSettings's data in an (optionally) encrypted binary format.
|
|
* SettingsSerializer can detect regular .ini files and serialized ones,
|
|
* it will read both regular and serialized .ini, but only save in serialized format.
|
|
* The file is encrypted with the current profile's password, if any.
|
|
* The file is only written to disk if save() is called, the destructor does not save to disk
|
|
* All member functions are reentrant, but not thread safe.
|
|
*
|
|
* @enum SettingsSerializer::RecordTag
|
|
* @var Value
|
|
* Followed by a QString key then a QVariant value
|
|
* @var GroupStart
|
|
* Followed by a QString group name
|
|
* @var ArrayStart
|
|
* Followed by a QString array name and a vuint array size
|
|
* @var ArrayValue
|
|
* Followed by a vuint array index, a QString key then a QVariant value
|
|
* @var ArrayEnd
|
|
* Not followed by any data
|
|
*/
|
|
enum class RecordTag : uint8_t
|
|
{
|
|
|
|
};
|
|
/**
|
|
* @var static const char magic[];
|
|
* @brief Little endian ASCII "QTOX" magic
|
|
*/
|
|
const char SettingsSerializer::magic[] = {0x51,0x54,0x4F,0x58};
|
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const SettingsSerializer::RecordTag& tag)
|
|
{
|
|
return dataStream << static_cast<uint8_t>(tag);
|
|
}
|
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const QByteArray& data)
|
|
{
|
|
QByteArray size = vintToData(data.size());
|
|
dataStream.writeRawData(size.data(), size.size());
|
|
dataStream.writeRawData(data.data(), data.size());
|
|
return dataStream;
|
|
}
|
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const QString& str)
|
|
{
|
|
return writeStream(dataStream, str.toUtf8());
|
|
}
|
|
|
|
QDataStream& readStream(QDataStream& dataStream, SettingsSerializer::RecordTag& tag)
|
|
{
|
|
return dataStream >> reinterpret_cast<quint8&>(tag) ;
|
|
}
|
|
|
|
|
|
QDataStream& readStream(QDataStream& dataStream, QByteArray& data)
|
|
{
|
|
char num3;
|
|
int num = 0;
|
|
int num2 = 0;
|
|
do
|
|
{
|
|
dataStream.readRawData(&num3, 1);
|
|
num |= (num3 & 0x7f) << num2;
|
|
num2 += 7;
|
|
} while ((num3 & 0x80) != 0);
|
|
data.resize(num);
|
|
dataStream.readRawData(data.data(), num);
|
|
return dataStream;
|
|
}
|
|
|
|
SettingsSerializer::SettingsSerializer(QString filePath, const QString &password)
|
|
: path{filePath}, password{password},
|
|
group{-1}, array{-1}, arrayIndex{-1}
|
|
{
|
|
}
|
|
|
|
void SettingsSerializer::beginGroup(const QString &prefix)
|
|
{
|
|
if (prefix.isEmpty())
|
|
endGroup();
|
|
int index = groups.indexOf(prefix);
|
|
if (index >= 0)
|
|
{
|
|
group = index;
|
|
}
|
|
else
|
|
{
|
|
group = groups.size();
|
|
groups.append(prefix);
|
|
}
|
|
}
|
|
|
|
void SettingsSerializer::endGroup()
|
|
{
|
|
group = -1;
|
|
}
|
|
|
|
int SettingsSerializer::beginReadArray(const QString &prefix)
|
|
{
|
|
auto index = std::find_if(std::begin(arrays), std::end(arrays),
|
|
[=](const Array& a)
|
|
{
|
|
return a.name==prefix;
|
|
});
|
|
|
|
if (index != std::end(arrays))
|
|
{
|
|
array = static_cast<int>(index - std::begin(arrays));
|
|
arrayIndex = -1;
|
|
return index->size;
|
|
}
|
|
else
|
|
{
|
|
array = arrays.size();
|
|
arrays.push_back({group, 0, prefix, {}});
|
|
arrayIndex = -1;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void SettingsSerializer::beginWriteArray(const QString &prefix, int size)
|
|
{
|
|
auto index = std::find_if(std::begin(arrays), std::end(arrays),
|
|
[=](const Array& a)
|
|
{
|
|
return a.name==prefix;
|
|
});
|
|
|
|
if (index != std::end(arrays))
|
|
{
|
|
array = static_cast<int>(index - std::begin(arrays));
|
|
arrayIndex = -1;
|
|
if (size > 0)
|
|
index->size = std::max(index->size, size);
|
|
}
|
|
else
|
|
{
|
|
if (size < 0)
|
|
size = 0;
|
|
array = arrays.size();
|
|
arrays.push_back({group, size, prefix, {}});
|
|
arrayIndex = -1;
|
|
}
|
|
}
|
|
|
|
void SettingsSerializer::endArray()
|
|
{
|
|
array = -1;
|
|
}
|
|
|
|
void SettingsSerializer::setArrayIndex(int i)
|
|
{
|
|
arrayIndex = i;
|
|
}
|
|
|
|
void SettingsSerializer::setValue(const QString &key, const QVariant &value)
|
|
{
|
|
Value* v = findValue(key);
|
|
if (v)
|
|
{
|
|
v->value = value;
|
|
}
|
|
else
|
|
{
|
|
Value nv{group, array, arrayIndex, key, value};
|
|
if (array >= 0)
|
|
arrays[array].values.append(values.size());
|
|
values.append(nv);
|
|
}
|
|
}
|
|
|
|
QVariant SettingsSerializer::value(const QString &key, const QVariant &defaultValue) const
|
|
{
|
|
const Value* v = findValue(key);
|
|
if (v)
|
|
return v->value;
|
|
else
|
|
return defaultValue;
|
|
}
|
|
|
|
const SettingsSerializer::Value* SettingsSerializer::findValue(const QString& key) const
|
|
{
|
|
if (array != -1)
|
|
{
|
|
for (const Array& a : arrays)
|
|
{
|
|
if (a.group != group)
|
|
continue;
|
|
|
|
for (int vi : a.values)
|
|
{
|
|
const Value& v = values[vi];
|
|
if (v.arrayIndex == arrayIndex && v.key == key)
|
|
return &v;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const Value& v : values)
|
|
if (v.group == group && v.array == -1 && v.key == key)
|
|
return &v;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
SettingsSerializer::Value* SettingsSerializer::findValue(const QString& key)
|
|
{
|
|
return const_cast<Value*>(const_cast<const SettingsSerializer*>(this)->findValue(key));
|
|
}
|
|
|
|
/**
|
|
* @brief Checks if the file is serialized settings.
|
|
* @param filePath Path to file to check.
|
|
* @return False on error, true otherwise.
|
|
*/
|
|
bool SettingsSerializer::isSerializedFormat(QString filePath)
|
|
{
|
|
QFile f(filePath);
|
|
if (!f.open(QIODevice::ReadOnly))
|
|
return false;
|
|
char fmagic[8];
|
|
if (f.read(fmagic, sizeof(fmagic)) != sizeof(fmagic))
|
|
return false;
|
|
return !memcmp(fmagic, magic, 4) || tox_is_data_encrypted(reinterpret_cast<uint8_t*>(fmagic));
|
|
}
|
|
|
|
/**
|
|
* @brief Loads the settings from file.
|
|
*/
|
|
void SettingsSerializer::load()
|
|
{
|
|
if (isSerializedFormat(path))
|
|
readSerialized();
|
|
else
|
|
readIni();
|
|
}
|
|
|
|
/**
|
|
* @brief Saves the current settings back to file
|
|
*/
|
|
void SettingsSerializer::save()
|
|
{
|
|
QSaveFile f(path);
|
|
if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly))
|
|
{
|
|
qWarning() << "Couldn't open file";
|
|
return;
|
|
}
|
|
|
|
QByteArray data(magic, 4);
|
|
QDataStream stream(&data, QIODevice::ReadWrite | QIODevice::Append);
|
|
stream.setVersion(QDataStream::Qt_5_0);
|
|
|
|
for (int g=-1; g<groups.size(); ++g)
|
|
{
|
|
// Save the group name, if any
|
|
if (g!=-1)
|
|
{
|
|
writeStream(stream, RecordTag::GroupStart);
|
|
writeStream(stream, groups[g].toUtf8());
|
|
}
|
|
|
|
// Save all the arrays of this group
|
|
for (const Array& a : arrays)
|
|
{
|
|
if (a.group != g)
|
|
continue;
|
|
if (a.size <= 0)
|
|
continue;
|
|
writeStream(stream, RecordTag::ArrayStart);
|
|
writeStream(stream, a.name.toUtf8());
|
|
writeStream(stream, vintToData(a.size));
|
|
|
|
for (int vi : a.values)
|
|
{
|
|
const Value& v = values[vi];
|
|
writeStream(stream, RecordTag::ArrayValue);
|
|
writeStream(stream, vintToData(values[vi].arrayIndex));
|
|
writeStream(stream, v.key.toUtf8());
|
|
writePackedVariant(stream, v.value);
|
|
}
|
|
writeStream(stream, RecordTag::ArrayEnd);
|
|
}
|
|
|
|
// Save all the values of this group that aren't in an array
|
|
for (const Value& v : values)
|
|
{
|
|
if (v.group != g || v.array != -1)
|
|
continue;
|
|
writeStream(stream, RecordTag::Value);
|
|
writeStream(stream, v.key.toUtf8());
|
|
writePackedVariant(stream, v.value);
|
|
}
|
|
}
|
|
|
|
// Encrypt
|
|
if (!password.isEmpty())
|
|
{
|
|
Core* core = Nexus::getCore();
|
|
auto passkey = core->createPasskey(password);
|
|
data = core->encryptData(data, *passkey);
|
|
}
|
|
|
|
f.write(data);
|
|
|
|
// check if everything got written
|
|
if (f.flush())
|
|
{
|
|
f.commit();
|
|
}
|
|
else
|
|
{
|
|
f.cancelWriting();
|
|
qCritical() << "Failed to write, can't save!";
|
|
}
|
|
}
|
|
|
|
void SettingsSerializer::readSerialized()
|
|
{
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::ReadOnly))
|
|
{
|
|
qWarning() << "Couldn't open file";
|
|
return;
|
|
}
|
|
QByteArray data = f.readAll();
|
|
f.close();
|
|
|
|
// Decrypt
|
|
if (tox_is_data_encrypted(reinterpret_cast<uint8_t*>(data.data())))
|
|
{
|
|
if (password.isEmpty())
|
|
{
|
|
qCritical() << "The settings file is encrypted, but we don't have a password!";
|
|
return;
|
|
}
|
|
|
|
Core* core = Nexus::getCore();
|
|
|
|
uint8_t salt[TOX_PASS_SALT_LENGTH];
|
|
tox_get_salt(reinterpret_cast<uint8_t *>(data.data()), salt, nullptr);
|
|
auto passkey = core->createPasskey(password, salt);
|
|
|
|
data = core->decryptData(data, *passkey);
|
|
if (data.isEmpty())
|
|
{
|
|
qCritical() << "Failed to decrypt the settings file";
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!password.isEmpty())
|
|
qWarning() << "We have a password, but the settings file is not encrypted";
|
|
}
|
|
|
|
if (memcmp(data.data(), magic, 4))
|
|
{
|
|
qWarning() << "Bad magic!";
|
|
return;
|
|
}
|
|
data = data.mid(4);
|
|
|
|
QDataStream stream(&data, QIODevice::ReadOnly);
|
|
stream.setVersion(QDataStream::Qt_5_0);
|
|
|
|
while (!stream.atEnd())
|
|
{
|
|
RecordTag tag;
|
|
readStream(stream, tag);
|
|
if (tag == RecordTag::Value)
|
|
{
|
|
QByteArray key;
|
|
QByteArray value;
|
|
readStream(stream, key);
|
|
readStream(stream, value);
|
|
setValue(QString::fromUtf8(key), QVariant(QString::fromUtf8(value)));
|
|
}
|
|
else if (tag == RecordTag::GroupStart)
|
|
{
|
|
QByteArray prefix;
|
|
readStream(stream, prefix);
|
|
beginGroup(QString::fromUtf8(prefix));
|
|
}
|
|
else if (tag == RecordTag::ArrayStart)
|
|
{
|
|
QByteArray prefix;
|
|
readStream(stream, prefix);
|
|
beginReadArray(QString::fromUtf8(prefix));
|
|
QByteArray sizeData;
|
|
readStream(stream, sizeData);
|
|
if (sizeData.isEmpty())
|
|
{
|
|
qWarning("The personal save file is corrupted!");
|
|
return;
|
|
}
|
|
int size = dataToVInt(sizeData);
|
|
arrays[array].size = qMax(size, arrays[array].size);
|
|
}
|
|
else if (tag == RecordTag::ArrayValue)
|
|
{
|
|
QByteArray indexData;
|
|
readStream(stream, indexData);
|
|
if (indexData.isEmpty())
|
|
{
|
|
qWarning("The personal save file is corrupted!");
|
|
return;
|
|
}
|
|
setArrayIndex(dataToVInt(indexData));
|
|
QByteArray key;
|
|
QByteArray value;
|
|
readStream(stream, key);
|
|
readStream(stream, value);
|
|
setValue(QString::fromUtf8(key), QVariant(QString::fromUtf8(value)));
|
|
}
|
|
else if (tag == RecordTag::ArrayEnd)
|
|
{
|
|
endArray();
|
|
}
|
|
}
|
|
|
|
group = array = -1;
|
|
}
|
|
|
|
void SettingsSerializer::readIni()
|
|
{
|
|
QSettings s(path, QSettings::IniFormat);
|
|
|
|
// Read all keys of all groups, reading arrays as raw keys
|
|
QList<QString> gstack;
|
|
do {
|
|
// Add all keys
|
|
if (!s.group().isEmpty())
|
|
beginGroup(s.group());
|
|
|
|
for (QString k : s.childKeys())
|
|
{
|
|
setValue(k, s.value(k));
|
|
}
|
|
|
|
// Add all groups
|
|
gstack.push_back(QString());
|
|
for (QString g : s.childGroups())
|
|
gstack.push_back(g);
|
|
|
|
// Visit the next group, if any
|
|
while (!gstack.isEmpty())
|
|
{
|
|
QString g = gstack.takeLast();
|
|
if (g.isEmpty())
|
|
{
|
|
if (gstack.isEmpty())
|
|
break;
|
|
else
|
|
s.endGroup();
|
|
}
|
|
else
|
|
{
|
|
s.beginGroup(g);
|
|
break;
|
|
}
|
|
}
|
|
} while (!gstack.isEmpty());
|
|
|
|
// We can convert keys that look like arrays into real arrays
|
|
// If a group's only key is called size, we'll consider it to be an array,
|
|
// and its elements are all groups matching the pattern "[<group>/]<arrayName>/<arrayIndex>"
|
|
|
|
// Find groups that only have 1 key
|
|
std::unique_ptr<int[]> groupSizes{new int[groups.size()]};
|
|
memset(groupSizes.get(), 0, static_cast<size_t>(groups.size()) * sizeof(int));
|
|
for (const Value& v : values)
|
|
{
|
|
if (v.group < 0 || v.group > groups.size())
|
|
continue;
|
|
groupSizes[static_cast<size_t>(v.group)]++;
|
|
}
|
|
|
|
// Find arrays, remove their size key from the values, and add them to `arrays`
|
|
QVector<int> groupsToKill;
|
|
for (int i=values.size()-1; i>=0; i--)
|
|
{
|
|
const Value& v = values[i];
|
|
if (v.group < 0 || v.group > groups.size())
|
|
continue;
|
|
if (groupSizes[static_cast<size_t>(v.group)] != 1)
|
|
continue;
|
|
if (v.key != "size")
|
|
continue;
|
|
if (!v.value.canConvert(QVariant::Int))
|
|
continue;
|
|
|
|
Array a;
|
|
a.size = v.value.toInt();
|
|
int slashIndex = groups[static_cast<int>(v.group)].lastIndexOf('/');
|
|
if (slashIndex == -1)
|
|
{
|
|
a.group = -1;
|
|
a.name = groups[static_cast<int>(v.group)];
|
|
a.size = v.value.toInt();
|
|
}
|
|
else
|
|
{
|
|
a.group = -1;
|
|
for (int i=0; i<groups.size(); ++i)
|
|
if (groups[i] == groups[static_cast<int>(v.group)].left(slashIndex))
|
|
a.group = i;
|
|
a.name = groups[static_cast<int>(v.group)].mid(slashIndex+1);
|
|
|
|
}
|
|
groupSizes[static_cast<size_t>(v.group)]--;
|
|
groupsToKill.append(static_cast<int>(v.group));
|
|
arrays.append(a);
|
|
values.removeAt(i);
|
|
}
|
|
|
|
// Associate each array's values with the array
|
|
for (int ai=0; ai<arrays.size(); ++ai)
|
|
{
|
|
Array& a = arrays[ai];
|
|
QString arrayPrefix;
|
|
if (a.group != -1)
|
|
arrayPrefix += groups[static_cast<int>(a.group)]+'/';
|
|
arrayPrefix += a.name+'/';
|
|
|
|
// Find groups which represent each array index
|
|
for (int g=0; g<groups.size(); ++g)
|
|
{
|
|
if (!groups[g].startsWith(arrayPrefix))
|
|
continue;
|
|
bool ok;
|
|
int groupArrayIndex = groups[g].mid(arrayPrefix.size()).toInt(&ok);
|
|
if (!ok)
|
|
continue;
|
|
groupsToKill.append(g);
|
|
|
|
if (groupArrayIndex > a.size)
|
|
a.size = groupArrayIndex;
|
|
|
|
// Associate the values for this array index
|
|
for (int vi = values.size() - 1; vi >= 0; vi--)
|
|
{
|
|
Value& v = values[vi];
|
|
if (v.group != g)
|
|
continue;
|
|
groupSizes[static_cast<size_t>(g)]--;
|
|
v.group = a.group;
|
|
v.array = ai;
|
|
v.arrayIndex = groupArrayIndex;
|
|
a.values.append(vi);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up spurious array element groups
|
|
std::sort(std::begin(groupsToKill), std::end(groupsToKill),
|
|
std::greater_equal<int>());
|
|
|
|
for (int g : groupsToKill)
|
|
{
|
|
if (groupSizes[static_cast<size_t>(g)])
|
|
continue;
|
|
|
|
removeGroup(g);
|
|
}
|
|
|
|
group = array = -1;
|
|
}
|
|
|
|
/**
|
|
* @brief Remove group.
|
|
* @note The group must be empty.
|
|
* @param group ID of group to remove.
|
|
*/
|
|
void SettingsSerializer::removeGroup(int group)
|
|
{
|
|
assert(group<groups.size());
|
|
for (Array& a : arrays)
|
|
{
|
|
assert(a.group != group);
|
|
if (a.group > group)
|
|
a.group--;
|
|
}
|
|
for (Value& v : values)
|
|
{
|
|
assert(v.group != group);
|
|
if (v.group > group)
|
|
v.group--;
|
|
}
|
|
groups.removeAt(group);
|
|
}
|
|
|
|
void SettingsSerializer::writePackedVariant(QDataStream& stream, const QVariant& v)
|
|
{
|
|
assert(v.canConvert(QVariant::String));
|
|
QString str = v.toString();
|
|
if (str == "true")
|
|
writeStream(stream, QString("1"));
|
|
else if (str == "false")
|
|
writeStream(stream, QString("0"));
|
|
else
|
|
writeStream(stream, str.toUtf8());
|
|
}
|