2015-06-06 21:54:58 +08:00
|
|
|
/*
|
|
|
|
Copyright © 2015 by The qTox Project
|
|
|
|
|
|
|
|
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"
|
2016-11-15 16:00:23 +08:00
|
|
|
#include "nexus.h"
|
|
|
|
#include "persistence/profile.h"
|
|
|
|
#include "core/core.h"
|
2016-03-24 08:10:09 +08:00
|
|
|
#include <QSaveFile>
|
2015-06-06 21:54:58 +08:00
|
|
|
#include <QFile>
|
|
|
|
#include <QDebug>
|
|
|
|
#include <memory>
|
|
|
|
#include <cassert>
|
|
|
|
|
2016-07-27 06:20:54 +08:00
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @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
|
|
|
|
*/
|
2016-07-27 06:20:54 +08:00
|
|
|
enum class RecordTag : uint8_t
|
|
|
|
{
|
|
|
|
|
|
|
|
};
|
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @var static const char magic[];
|
|
|
|
* @brief Little endian ASCII "QTOX" magic
|
|
|
|
*/
|
2015-06-07 17:34:30 +08:00
|
|
|
const char SettingsSerializer::magic[] = {0x51,0x54,0x4F,0x58};
|
2015-06-06 21:54:58 +08:00
|
|
|
|
2016-01-19 23:28:42 +08:00
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const SettingsSerializer::RecordTag& tag)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
return dataStream << static_cast<uint8_t>(tag);
|
|
|
|
}
|
|
|
|
|
2016-01-19 23:28:42 +08:00
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const QByteArray& data)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
QByteArray size = vintToData(data.size());
|
2015-06-06 21:54:58 +08:00
|
|
|
dataStream.writeRawData(size.data(), size.size());
|
|
|
|
dataStream.writeRawData(data.data(), data.size());
|
|
|
|
return dataStream;
|
|
|
|
}
|
|
|
|
|
2016-01-19 23:28:42 +08:00
|
|
|
QDataStream& writeStream(QDataStream& dataStream, const QString& str)
|
|
|
|
{
|
|
|
|
return writeStream(dataStream, str.toUtf8());
|
|
|
|
}
|
|
|
|
|
|
|
|
QDataStream& readStream(QDataStream& dataStream, SettingsSerializer::RecordTag& tag)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
return dataStream >> reinterpret_cast<quint8&>(tag) ;
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
2016-01-19 23:28:42 +08:00
|
|
|
|
|
|
|
QDataStream& readStream(QDataStream& dataStream, QByteArray& data)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
char num3;
|
|
|
|
int num = 0;
|
2015-06-06 21:54:58 +08:00
|
|
|
int num2 = 0;
|
|
|
|
do
|
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
dataStream.readRawData(&num3, 1);
|
2015-06-06 21:54:58 +08:00
|
|
|
num |= (num3 & 0x7f) << num2;
|
|
|
|
num2 += 7;
|
|
|
|
} while ((num3 & 0x80) != 0);
|
|
|
|
data.resize(num);
|
|
|
|
dataStream.readRawData(data.data(), num);
|
|
|
|
return dataStream;
|
|
|
|
}
|
|
|
|
|
2016-06-18 12:49:40 +08:00
|
|
|
SettingsSerializer::SettingsSerializer(QString filePath, const QString &password)
|
2015-06-06 21:54:58 +08:00
|
|
|
: 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)
|
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
auto index = std::find_if(std::begin(arrays), std::end(arrays),
|
|
|
|
[=](const Array& a)
|
|
|
|
{
|
|
|
|
return a.name==prefix;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (index != std::end(arrays))
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
array = static_cast<int>(index - std::begin(arrays));
|
2015-06-06 21:54:58 +08:00
|
|
|
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)
|
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
auto index = std::find_if(std::begin(arrays), std::end(arrays),
|
|
|
|
[=](const Array& a)
|
|
|
|
{
|
|
|
|
return a.name==prefix;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (index != std::end(arrays))
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
array = static_cast<int>(index - std::begin(arrays));
|
2015-06-06 21:54:58 +08:00
|
|
|
arrayIndex = -1;
|
|
|
|
if (size > 0)
|
2016-08-01 03:24:48 +08:00
|
|
|
index->size = std::max(index->size, size);
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (size < 0)
|
|
|
|
size = 0;
|
|
|
|
array = arrays.size();
|
2016-08-01 03:24:48 +08:00
|
|
|
arrays.push_back({group, size, prefix, {}});
|
2015-06-06 21:54:58 +08:00
|
|
|
arrayIndex = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SettingsSerializer::endArray()
|
|
|
|
{
|
|
|
|
array = -1;
|
|
|
|
}
|
|
|
|
|
2016-08-01 03:24:48 +08:00
|
|
|
void SettingsSerializer::setArrayIndex(int i)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
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;
|
2016-08-01 03:24:48 +08:00
|
|
|
|
2015-06-06 21:54:58 +08:00
|
|
|
for (int vi : a.values)
|
2016-08-01 03:24:48 +08:00
|
|
|
{
|
|
|
|
const Value& v = values[vi];
|
|
|
|
if (v.arrayIndex == arrayIndex && v.key == key)
|
|
|
|
return &v;
|
|
|
|
}
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2016-07-27 06:20:54 +08:00
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @brief Checks if the file is serialized settings.
|
|
|
|
* @param filePath Path to file to check.
|
|
|
|
* @return False on error, true otherwise.
|
|
|
|
*/
|
2015-06-06 21:54:58 +08:00
|
|
|
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;
|
2016-08-01 03:24:48 +08:00
|
|
|
return !memcmp(fmagic, magic, 4) || tox_is_data_encrypted(reinterpret_cast<uint8_t*>(fmagic));
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
2016-07-27 06:20:54 +08:00
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @brief Loads the settings from file.
|
|
|
|
*/
|
2015-06-07 07:13:07 +08:00
|
|
|
void SettingsSerializer::load()
|
|
|
|
{
|
|
|
|
if (isSerializedFormat(path))
|
|
|
|
readSerialized();
|
|
|
|
else
|
|
|
|
readIni();
|
|
|
|
}
|
|
|
|
|
2016-07-27 06:20:54 +08:00
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @brief Saves the current settings back to file
|
|
|
|
*/
|
2015-06-07 07:13:07 +08:00
|
|
|
void SettingsSerializer::save()
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-03-24 08:10:09 +08:00
|
|
|
QSaveFile f(path);
|
2015-06-06 21:54:58 +08:00
|
|
|
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);
|
|
|
|
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int g=-1; g<groups.size(); ++g)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
// Save the group name, if any
|
|
|
|
if (g!=-1)
|
|
|
|
{
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, RecordTag::GroupStart);
|
|
|
|
writeStream(stream, groups[g].toUtf8());
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Save all the arrays of this group
|
|
|
|
for (const Array& a : arrays)
|
|
|
|
{
|
|
|
|
if (a.group != g)
|
|
|
|
continue;
|
|
|
|
if (a.size <= 0)
|
|
|
|
continue;
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, RecordTag::ArrayStart);
|
|
|
|
writeStream(stream, a.name.toUtf8());
|
2016-08-01 03:24:48 +08:00
|
|
|
writeStream(stream, vintToData(a.size));
|
2016-01-19 23:28:42 +08:00
|
|
|
|
2016-08-01 03:24:48 +08:00
|
|
|
for (int vi : a.values)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
const Value& v = values[vi];
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, RecordTag::ArrayValue);
|
2016-08-01 03:24:48 +08:00
|
|
|
writeStream(stream, vintToData(values[vi].arrayIndex));
|
|
|
|
writeStream(stream, v.key.toUtf8());
|
|
|
|
writePackedVariant(stream, v.value);
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, RecordTag::ArrayEnd);
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, RecordTag::Value);
|
|
|
|
writeStream(stream, v.key.toUtf8());
|
2015-06-06 21:54:58 +08:00
|
|
|
writePackedVariant(stream, v.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encrypt
|
|
|
|
if (!password.isEmpty())
|
|
|
|
{
|
|
|
|
Core* core = Nexus::getCore();
|
2015-06-28 03:14:35 +08:00
|
|
|
auto passkey = core->createPasskey(password);
|
|
|
|
data = core->encryptData(data, *passkey);
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
f.write(data);
|
2016-03-24 08:10:09 +08:00
|
|
|
|
|
|
|
// check if everything got written
|
2016-07-07 18:08:32 +08:00
|
|
|
if (f.flush())
|
2016-03-24 08:10:09 +08:00
|
|
|
{
|
|
|
|
f.commit();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
f.cancelWriting();
|
|
|
|
qCritical() << "Failed to write, can't save!";
|
|
|
|
}
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void SettingsSerializer::readSerialized()
|
|
|
|
{
|
|
|
|
QFile f(path);
|
|
|
|
if (!f.open(QIODevice::ReadOnly))
|
|
|
|
{
|
|
|
|
qWarning() << "Couldn't open file";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
QByteArray data = f.readAll();
|
|
|
|
f.close();
|
|
|
|
|
|
|
|
// Decrypt
|
2016-08-01 03:24:48 +08:00
|
|
|
if (tox_is_data_encrypted(reinterpret_cast<uint8_t*>(data.data())))
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
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);
|
2015-06-28 03:14:35 +08:00
|
|
|
auto passkey = core->createPasskey(password, salt);
|
2015-06-06 21:54:58 +08:00
|
|
|
|
2015-06-28 03:14:35 +08:00
|
|
|
data = core->decryptData(data, *passkey);
|
2015-06-06 21:54:58 +08:00
|
|
|
if (data.isEmpty())
|
2015-06-28 03:14:35 +08:00
|
|
|
{
|
2015-06-06 21:54:58 +08:00
|
|
|
qCritical() << "Failed to decrypt the settings file";
|
2015-06-28 03:14:35 +08:00
|
|
|
return;
|
|
|
|
}
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
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;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, tag);
|
2015-06-06 21:54:58 +08:00
|
|
|
if (tag == RecordTag::Value)
|
|
|
|
{
|
|
|
|
QByteArray key;
|
|
|
|
QByteArray value;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, key);
|
|
|
|
readStream(stream, value);
|
2015-06-06 21:54:58 +08:00
|
|
|
setValue(QString::fromUtf8(key), QVariant(QString::fromUtf8(value)));
|
|
|
|
}
|
|
|
|
else if (tag == RecordTag::GroupStart)
|
|
|
|
{
|
|
|
|
QByteArray prefix;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, prefix);
|
2015-06-06 21:54:58 +08:00
|
|
|
beginGroup(QString::fromUtf8(prefix));
|
|
|
|
}
|
|
|
|
else if (tag == RecordTag::ArrayStart)
|
|
|
|
{
|
|
|
|
QByteArray prefix;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, prefix);
|
2015-06-06 21:54:58 +08:00
|
|
|
beginReadArray(QString::fromUtf8(prefix));
|
|
|
|
QByteArray sizeData;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, sizeData);
|
2015-06-07 02:53:31 +08:00
|
|
|
if (sizeData.isEmpty())
|
|
|
|
{
|
2016-01-17 03:08:43 +08:00
|
|
|
qWarning("The personal save file is corrupted!");
|
2015-06-07 02:53:31 +08:00
|
|
|
return;
|
|
|
|
}
|
2016-08-01 03:24:48 +08:00
|
|
|
int size = dataToVInt(sizeData);
|
|
|
|
arrays[array].size = qMax(size, arrays[array].size);
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
else if (tag == RecordTag::ArrayValue)
|
|
|
|
{
|
|
|
|
QByteArray indexData;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, indexData);
|
2015-06-07 02:53:31 +08:00
|
|
|
if (indexData.isEmpty())
|
|
|
|
{
|
2016-01-17 03:08:43 +08:00
|
|
|
qWarning("The personal save file is corrupted!");
|
2015-06-07 02:53:31 +08:00
|
|
|
return;
|
|
|
|
}
|
2016-08-01 03:24:48 +08:00
|
|
|
setArrayIndex(dataToVInt(indexData));
|
2015-06-06 21:54:58 +08:00
|
|
|
QByteArray key;
|
|
|
|
QByteArray value;
|
2016-01-19 23:28:42 +08:00
|
|
|
readStream(stream, key);
|
|
|
|
readStream(stream, value);
|
2015-06-06 21:54:58 +08:00
|
|
|
setValue(QString::fromUtf8(key), QVariant(QString::fromUtf8(value)));
|
|
|
|
}
|
|
|
|
else if (tag == RecordTag::ArrayEnd)
|
|
|
|
{
|
|
|
|
endArray();
|
|
|
|
}
|
|
|
|
}
|
2015-06-10 21:14:36 +08:00
|
|
|
|
|
|
|
group = array = -1;
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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());
|
2016-07-07 18:08:32 +08:00
|
|
|
|
2015-06-06 21:54:58 +08:00
|
|
|
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()]};
|
2016-08-01 03:24:48 +08:00
|
|
|
memset(groupSizes.get(), 0, static_cast<size_t>(groups.size()) * sizeof(int));
|
2015-06-06 21:54:58 +08:00
|
|
|
for (const Value& v : values)
|
|
|
|
{
|
|
|
|
if (v.group < 0 || v.group > groups.size())
|
|
|
|
continue;
|
2016-08-01 03:24:48 +08:00
|
|
|
groupSizes[static_cast<size_t>(v.group)]++;
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2016-08-01 03:24:48 +08:00
|
|
|
if (groupSizes[static_cast<size_t>(v.group)] != 1)
|
2015-06-06 21:54:58 +08:00
|
|
|
continue;
|
|
|
|
if (v.key != "size")
|
|
|
|
continue;
|
|
|
|
if (!v.value.canConvert(QVariant::Int))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
Array a;
|
|
|
|
a.size = v.value.toInt();
|
2016-08-01 03:24:48 +08:00
|
|
|
int slashIndex = groups[static_cast<int>(v.group)].lastIndexOf('/');
|
2015-06-06 21:54:58 +08:00
|
|
|
if (slashIndex == -1)
|
|
|
|
{
|
|
|
|
a.group = -1;
|
2016-08-01 03:24:48 +08:00
|
|
|
a.name = groups[static_cast<int>(v.group)];
|
2015-06-06 21:54:58 +08:00
|
|
|
a.size = v.value.toInt();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
a.group = -1;
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int i=0; i<groups.size(); ++i)
|
2016-08-01 03:24:48 +08:00
|
|
|
if (groups[i] == groups[static_cast<int>(v.group)].left(slashIndex))
|
2015-06-06 21:54:58 +08:00
|
|
|
a.group = i;
|
2016-08-01 03:24:48 +08:00
|
|
|
a.name = groups[static_cast<int>(v.group)].mid(slashIndex+1);
|
2015-06-06 21:54:58 +08:00
|
|
|
|
|
|
|
}
|
2016-08-01 03:24:48 +08:00
|
|
|
groupSizes[static_cast<size_t>(v.group)]--;
|
|
|
|
groupsToKill.append(static_cast<int>(v.group));
|
2015-06-06 21:54:58 +08:00
|
|
|
arrays.append(a);
|
|
|
|
values.removeAt(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Associate each array's values with the array
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int ai=0; ai<arrays.size(); ++ai)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
Array& a = arrays[ai];
|
|
|
|
QString arrayPrefix;
|
|
|
|
if (a.group != -1)
|
2016-08-01 03:24:48 +08:00
|
|
|
arrayPrefix += groups[static_cast<int>(a.group)]+'/';
|
2015-06-06 21:54:58 +08:00
|
|
|
arrayPrefix += a.name+'/';
|
|
|
|
|
|
|
|
// Find groups which represent each array index
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int g=0; g<groups.size(); ++g)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
if (!groups[g].startsWith(arrayPrefix))
|
|
|
|
continue;
|
|
|
|
bool ok;
|
2016-08-01 03:24:48 +08:00
|
|
|
int groupArrayIndex = groups[g].mid(arrayPrefix.size()).toInt(&ok);
|
2015-06-06 21:54:58 +08:00
|
|
|
if (!ok)
|
|
|
|
continue;
|
|
|
|
groupsToKill.append(g);
|
|
|
|
|
|
|
|
if (groupArrayIndex > a.size)
|
|
|
|
a.size = groupArrayIndex;
|
|
|
|
|
|
|
|
// Associate the values for this array index
|
2016-08-01 03:24:48 +08:00
|
|
|
for (int vi = values.size() - 1; vi >= 0; vi--)
|
2015-06-06 21:54:58 +08:00
|
|
|
{
|
|
|
|
Value& v = values[vi];
|
|
|
|
if (v.group != g)
|
|
|
|
continue;
|
2016-08-01 03:24:48 +08:00
|
|
|
groupSizes[static_cast<size_t>(g)]--;
|
2015-06-06 21:54:58 +08:00
|
|
|
v.group = a.group;
|
|
|
|
v.array = ai;
|
|
|
|
v.arrayIndex = groupArrayIndex;
|
|
|
|
a.values.append(vi);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up spurious array element groups
|
2016-08-01 03:24:48 +08:00
|
|
|
std::sort(std::begin(groupsToKill), std::end(groupsToKill),
|
|
|
|
std::greater_equal<int>());
|
|
|
|
|
2015-06-06 21:54:58 +08:00
|
|
|
for (int g : groupsToKill)
|
|
|
|
{
|
2016-08-01 03:24:48 +08:00
|
|
|
if (groupSizes[static_cast<size_t>(g)])
|
2015-06-06 21:54:58 +08:00
|
|
|
continue;
|
2016-07-07 18:08:32 +08:00
|
|
|
|
2015-06-06 21:54:58 +08:00
|
|
|
removeGroup(g);
|
|
|
|
}
|
2015-06-10 21:14:36 +08:00
|
|
|
|
|
|
|
group = array = -1;
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|
|
|
|
|
2016-07-27 06:20:54 +08:00
|
|
|
/**
|
2016-08-01 16:21:23 +08:00
|
|
|
* @brief Remove group.
|
|
|
|
* @note The group must be empty.
|
|
|
|
* @param group ID of group to remove.
|
2016-07-27 06:20:54 +08:00
|
|
|
*/
|
2015-06-06 21:54:58 +08:00
|
|
|
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")
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, QString("1"));
|
2015-06-06 21:54:58 +08:00
|
|
|
else if (str == "false")
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, QString("0"));
|
2015-06-06 21:54:58 +08:00
|
|
|
else
|
2016-01-19 23:28:42 +08:00
|
|
|
writeStream(stream, str.toUtf8());
|
2015-06-06 21:54:58 +08:00
|
|
|
}
|