1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/src/persistence/settingsserializer.cpp
Zetok Zalbavar d4ac13dbf4
revert: "refactor: Added to include path and exclude it from all includes"
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 commit b4a9f04f92.
This reverts commit 5921122960.
2016-12-29 16:10:53 +00:00

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());
}