Page MenuHomePhorge

No OneTemporary

Size
51 KB
Referenced Files
None
Subscribers
None
diff --git a/src/base/serialization/std-optional.hpp b/src/base/serialization/std-optional.hpp
new file mode 100644
index 0000000..80d763f
--- /dev/null
+++ b/src/base/serialization/std-optional.hpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 Tusooa Zhu <tusooa@kazv.moe>
+ *
+ * This file is part of libkazv.
+ *
+ * libkazv is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * libkazv 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with libkazv. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+#include <libkazv-config.hpp>
+
+#include <boost/serialization/nvp.hpp>
+
+#include <optional>
+
+namespace boost::serialization
+{
+ template<class Archive, class T>
+ void save(Archive &ar, const std::optional<T> &o, const unsigned int /* version */)
+ {
+ auto hasValue = o.has_value();
+ ar << BOOST_SERIALIZATION_NVP(hasValue);
+
+ if (hasValue) {
+ const auto &value = o.value();
+ ar << BOOST_SERIALIZATION_NVP(value);
+ }
+ }
+
+ template<class Archive, class T>
+ void load(Archive &ar, std::optional<T> &o, const unsigned int /* version */)
+ {
+ auto hasValue = bool{};
+ ar >> BOOST_SERIALIZATION_NVP(hasValue);
+
+ o.reset();
+
+ if (hasValue) {
+ T value;
+ ar >> BOOST_SERIALIZATION_NVP(value);
+ o = std::move(value);
+ }
+ }
+
+ template<class Archive,
+ class T>
+ inline void serialize(Archive &ar, std::optional<T> &o, const unsigned int version)
+ {
+ boost::serialization::split_free(ar, o, version);
+ }
+}
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
index 42c5bd9..500e74d 100644
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -1,262 +1,257 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#include <libkazv-config.hpp>
#include <lager/util.hpp>
#include <lager/context.hpp>
#include <functional>
#include <zug/transducer/filter.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "client-model.hpp"
#include "actions/states.hpp"
#include "actions/auth.hpp"
#include "actions/membership.hpp"
#include "actions/paginate.hpp"
#include "actions/send.hpp"
#include "actions/states.hpp"
#include "actions/sync.hpp"
#include "actions/ephemeral.hpp"
#include "actions/content.hpp"
#include "actions/encryption.hpp"
namespace Kazv
{
auto ClientModel::update(ClientModel m, Action a) -> Result
{
auto oldDeviceLists = m.deviceLists;
auto [newClient, effect] = lager::match(std::move(a))(
- [&](Error::Action a) -> Result {
- m.error = Error::update(m.error, a);
- return {std::move(m), lager::noop};
- },
-
[&](RoomListAction a) -> Result {
m.roomList = RoomListModel::update(std::move(m.roomList), a);
return {std::move(m), lager::noop};
},
[&](ResubmitJobAction a) -> Result {
m.addJob(std::move(a.job));
return { std::move(m), lager::noop };
},
[&](auto a) -> decltype(updateClient(m, a)) {
return updateClient(m, a);
},
#define RESPONSE_FOR(_jobId) \
if (r.jobId() == #_jobId) { \
return processResponse(m, _jobId##Response{std::move(r)}); \
}
[&](ProcessResponseAction a) -> Result {
auto r = std::move(a.response);
// auth
RESPONSE_FOR(Login);
// paginate
RESPONSE_FOR(GetRoomEvents);
// sync
RESPONSE_FOR(Sync);
RESPONSE_FOR(DefineFilter);
// membership
RESPONSE_FOR(CreateRoom);
RESPONSE_FOR(InviteUser);
RESPONSE_FOR(JoinRoomById);
RESPONSE_FOR(JoinRoom);
RESPONSE_FOR(LeaveRoom);
RESPONSE_FOR(ForgetRoom);
// send
RESPONSE_FOR(SendMessage);
RESPONSE_FOR(SendToDevice);
// states
RESPONSE_FOR(GetRoomState);
RESPONSE_FOR(SetRoomStateWithKey);
RESPONSE_FOR(GetRoomStateWithKey);
// ephemeral
RESPONSE_FOR(SetTyping);
RESPONSE_FOR(PostReceipt);
RESPONSE_FOR(SetReadMarker);
// content
RESPONSE_FOR(UploadContent);
RESPONSE_FOR(GetContent);
RESPONSE_FOR(GetContentThumbnail);
// encryption
RESPONSE_FOR(UploadKeys);
RESPONSE_FOR(QueryKeys);
RESPONSE_FOR(ClaimKeys);
m.addTrigger(UnrecognizedResponse{std::move(r)});
return { std::move(m), lager::noop };
}
#undef RESPONSE_FOR
);
// Rotate megolm keys for rooms whose users' device list has changed
auto changedUsers = newClient.deviceLists.diff(oldDeviceLists);
if (! changedUsers.empty()) {
for (auto [roomId, room] : newClient.roomList.rooms) {
auto it = std::find_if(changedUsers.begin(), changedUsers.end(),
[=](auto userId) { return room.hasUser(userId); });
if (it != changedUsers.end()) {
newClient.roomList.rooms =
std::move(newClient.roomList.rooms)
.update(roomId, [](auto room) {
room.shouldRotateSessionKey = true;
return room;
});
}
}
}
return { std::move(newClient), std::move(effect) };
}
std::pair<Event, std::optional<std::string>> ClientModel::megOlmEncrypt(Event e, std::string roomId)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return { e, std::nullopt };
}
auto &c = crypto.value();
auto j = e.originalJson().get();
auto r = roomList[roomId];
if (! r.encrypted) {
kzo.client.dbg() << "The room " << roomId
<< " is not encrypted, so do not encrypt events" << std::endl;
return { e, std::nullopt };
}
auto desc = r.sessionRotateDesc();
auto keyOpt = std::optional<std::string>{};
if (r.shouldRotateSessionKey) {
kzo.client.dbg() << "We should rotate this session." << std::endl;
keyOpt = c.rotateMegOlmSession(roomId);
} else {
keyOpt = c.rotateMegOlmSessionIfNeeded(roomId, desc);
}
// we no longer need to rotate session
// until next time a device change happens
roomList.rooms = std::move(roomList.rooms)
.update(roomId, [](auto r) { r.shouldRotateSessionKey = false; return r; });
// so that Crypto::encryptMegOlm() can find room id
j["room_id"] = roomId;
auto content = c.encryptMegOlm(j);
j["type"] = "m.room.encrypted";
j["content"] = std::move(content);
j["content"]["device_id"] = deviceId;
kzo.client.dbg() << "Encrypted json is " << j.dump() << std::endl;
kzo.client.dbg() << "Session key is " << (keyOpt ? keyOpt.value() : "<not rotated>") << std::endl;
return { Event(JsonWrap(j)), keyOpt };
}
Event ClientModel::olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap)
{
if (!crypto) {
kzo.client.dbg() << "We do not have e2ee, so do not encrypt events" << std::endl;
return e;
}
if (e.encrypted()) {
kzo.client.dbg() << "The event is already encrypted. Ignoring it." << std::endl;
return e;
}
auto &c = crypto.value();
auto origJson = e.originalJson().get();
auto encJson = json::object();
encJson["content"] = json{
{"algorithm", CryptoConstants::olmAlgo},
{"ciphertext", json::object()},
{"sender_key", c.curve25519IdentityKey()},
};
encJson["type"] = "m.room.encrypted";
for (auto [userId, devices] : userIdToDeviceIdMap) {
for (auto dev : devices) {
auto devInfoOpt = deviceLists.get(userId, dev);
if (! devInfoOpt) {
continue;
}
auto devInfo = devInfoOpt.value();
auto jsonForThisDevice = origJson;
jsonForThisDevice["sender"] = this->userId;
jsonForThisDevice["recipient"] = userId;
jsonForThisDevice["recipient_keys"] = json{
{CryptoConstants::ed25519, devInfo.ed25519Key}
};
jsonForThisDevice["keys"] = json{
{CryptoConstants::ed25519, c.ed25519IdentityKey()}
};
encJson["content"]["ciphertext"]
.merge_patch(c.encryptOlm(jsonForThisDevice, devInfo.curve25519Key));
}
}
return Event(JsonWrap(encJson));
}
immer::flex_vector<std::string /* deviceId */> ClientModel::devicesToSendKeys(std::string userId) const
{
auto trustLevelNeeded = DeviceTrustLevel::Unseen;
// XXX: preliminary approach
auto shouldSendP = [=](auto deviceInfo, auto /* deviceMap */) {
return deviceInfo.trustLevel >= trustLevelNeeded;
};
auto devices = deviceLists.devicesFor(userId);
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([=](auto n) {
auto [id, dev] = n;
return shouldSendP(dev, devices);
})
| zug::map([=](auto n) {
return n.first;
}),
devices);
}
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index 9fe9ee5..ad9b304 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,393 +1,417 @@
/*
- * Copyright (C) 2020 Tusooa Zhu
+ * Copyright (C) 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <string>
#include <optional>
#include <lager/context.hpp>
#include <boost/hana.hpp>
+#include <serialization/std-optional.hpp>
+
#ifndef NDEBUG
#include <lager/debug/cereal/struct.hpp>
#endif
#include <csapi/sync.hpp>
#include <file-desc.hpp>
#include <crypto.hpp>
+#include <serialization/immer-flex-vector.hpp>
+#include <serialization/immer-box.hpp>
+#include <serialization/immer-map.hpp>
+#include <serialization/immer-array.hpp>
+
#include "clientfwd.hpp"
#include "device-list-tracker.hpp"
-#include "error.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
inline const std::string DEFTXNID{"0"};
enum RoomVisibility
{
Private,
Public,
};
enum CreateRoomPreset
{
PrivateChat,
PublicChat,
TrustedPrivateChat,
};
enum ThumbnailResizingMethod
{
Crop,
Scale,
};
struct ClientModel
{
std::string serverUrl;
std::string userId;
std::string token;
std::string deviceId;
bool loggedIn{false};
- Error error;
bool syncing{false};
int firstRetryMs{1000};
int retryTimeFactor{2};
int maxRetryMs{30 * 1000};
int syncTimeoutMs{20000};
std::string initialSyncFilterId;
std::string incrementalSyncFilterId;
std::optional<std::string> syncToken;
RoomListModel roomList;
immer::map<std::string /* sender */, Event> presence;
immer::map<std::string /* type */, Event> accountData;
std::string nextTxnId{DEFTXNID};
immer::flex_vector<BaseJob> nextJobs;
immer::flex_vector<KazvEvent> nextTriggers;
EventList toDevice;
std::optional<Crypto> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
immer::flex_vector<std::string /* deviceId */> devicesToSendKeys(std::string userId) const;
std::pair<Event, std::optional<std::string> /* sessionKey */> megOlmEncrypt(Event e, std::string roomId);
/// precondition: the one-time keys for those devices must already be claimed
Event olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap);
// helpers
template<class Job>
struct MakeJobT
{
template<class ...Args>
constexpr auto make(Args &&...args) const {
if constexpr (Job::needsAuth()) {
return Job(
serverUrl,
token,
std::forward<Args>(args)...);
} else {
return Job(
serverUrl,
std::forward<Args>(args)...);
}
}
std::string serverUrl;
std::string token;
};
template<class Job>
constexpr auto job() const {
return MakeJobT<Job>{serverUrl, token};
}
inline void addJob(BaseJob j) {
nextJobs = std::move(nextJobs).push_back(std::move(j));
}
inline auto popAllJobs() {
auto jobs = std::move(nextJobs);
nextJobs = DEFVAL;
return jobs;
};
inline void addTrigger(KazvEvent t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvEvent> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
static Result update(ClientModel m, Action a);
};
// actions:
struct LoginAction {
std::string serverUrl;
std::string username;
std::string password;
std::optional<std::string> deviceName;
};
struct TokenLoginAction
{
std::string serverUrl;
std::string username;
std::string token;
std::string deviceId;
};
struct LogoutAction {};
struct SyncAction {};
struct PaginateTimelineAction
{
std::string roomId;
/// Must be where the Gap is
std::string fromEventId;
std::optional<int> limit;
};
struct SendMessageAction
{
std::string roomId;
Event event;
};
struct SendStateEventAction
{
std::string roomId;
Event event;
};
struct CreateRoomAction
{
using Visibility = RoomVisibility;
using Preset = CreateRoomPreset;
Visibility visibility;
std::optional<std::string> roomAliasName;
std::optional<std::string> name;
std::optional<std::string> topic;
immer::array<std::string> invite;
//immer::array<Invite3pid> invite3pid;
std::optional<std::string> roomVersion;
JsonWrap creationContent;
immer::array<Event> initialState;
std::optional<Preset> preset;
std::optional<bool> isDirect;
JsonWrap powerLevelContentOverride;
};
struct GetRoomStatesAction
{
std::string roomId;
};
struct GetStateEventAction
{
std::string roomId;
std::string type;
std::string stateKey;
};
struct InviteToRoomAction
{
std::string roomId;
std::string userId;
};
struct JoinRoomByIdAction
{
std::string roomId;
};
struct JoinRoomAction
{
std::string roomIdOrAlias;
immer::array<std::string> serverName;
};
struct LeaveRoomAction
{
std::string roomId;
};
struct ForgetRoomAction
{
std::string roomId;
};
struct SetTypingAction
{
std::string roomId;
bool typing;
std::optional<int> timeoutMs;
};
struct PostReceiptAction
{
std::string roomId;
std::string eventId;
};
struct SetReadMarkerAction
{
std::string roomId;
std::string eventId;
};
struct UploadContentAction
{
FileDesc content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
std::string uploadId; // to be used by library users
};
struct DownloadContentAction
{
std::string mxcUri;
std::optional<FileDesc> downloadTo;
};
struct DownloadThumbnailAction
{
std::string mxcUri;
int width;
int height;
std::optional<ThumbnailResizingMethod> method;
std::optional<bool> allowRemote;
std::optional<FileDesc> downloadTo;
};
struct ResubmitJobAction
{
BaseJob job;
};
struct ProcessResponseAction
{
Response response;
};
struct PostInitialFiltersAction
{
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
struct UploadIdentityKeysAction
{
};
struct GenerateAndUploadOneTimeKeysAction
{
};
struct QueryKeysAction
{
bool isInitialSync;
};
struct ClaimKeysAndSendSessionKeyAction
{
std::string roomId;
std::string sessionId;
std::string sessionKey;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
#ifndef NDEBUG
LAGER_CEREAL_STRUCT(LoginAction);
LAGER_CEREAL_STRUCT(TokenLoginAction);
LAGER_CEREAL_STRUCT(LogoutAction);
LAGER_CEREAL_STRUCT(SyncAction);
LAGER_CEREAL_STRUCT(PostInitialFiltersAction);
LAGER_CEREAL_STRUCT(PaginateTimelineAction);
LAGER_CEREAL_STRUCT(SendMessageAction);
LAGER_CEREAL_STRUCT(SendStateEventAction);
LAGER_CEREAL_STRUCT(SendToDeviceMessageAction);
LAGER_CEREAL_STRUCT(CreateRoomAction);
LAGER_CEREAL_STRUCT(GetRoomStatesAction);
LAGER_CEREAL_STRUCT(GetStateEventAction);
LAGER_CEREAL_STRUCT(InviteToRoomAction);
LAGER_CEREAL_STRUCT(JoinRoomByIdAction);
LAGER_CEREAL_STRUCT(JoinRoomAction);
LAGER_CEREAL_STRUCT(LeaveRoomAction);
LAGER_CEREAL_STRUCT(ForgetRoomAction);
LAGER_CEREAL_STRUCT(SetTypingAction);
LAGER_CEREAL_STRUCT(PostReceiptAction);
LAGER_CEREAL_STRUCT(ProcessResponseAction);
LAGER_CEREAL_STRUCT(SetReadMarkerAction);
LAGER_CEREAL_STRUCT(UploadContentAction);
LAGER_CEREAL_STRUCT(DownloadContentAction);
LAGER_CEREAL_STRUCT(DownloadThumbnailAction);
LAGER_CEREAL_STRUCT(ResubmitJobAction);
#endif
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const /*version*/)
{
- ar(m.serverUrl, m.userId, m.token, m.deviceId, m.loggedIn,
- m.error,
- m.initialSyncFilterId,
- m.incrementalSyncFilterId,
- m.syncToken,
- m.roomList,
- m.presence,
- m.accountData,
- m.nextTxnId,
- m.toDevice);
+ ar
+ & m.serverUrl
+ & m.userId
+ & m.token
+ & m.deviceId
+ & m.loggedIn
+
+ & m.syncing
+ & m.firstRetryMs
+ & m.retryTimeFactor
+ & m.maxRetryMs
+ & m.syncTimeoutMs
+ & m.initialSyncFilterId
+ & m.incrementalSyncFilterId
+ & m.syncToken
+
+ & m.roomList
+ & m.presence
+ & m.accountData
+
+ & m.nextTxnId
+
+ & m.toDevice
+ & m.crypto
+ & m.identityKeysUploaded
+
+ & m.deviceLists
+ ;
}
}
-CEREAL_CLASS_VERSION(Kazv::ClientModel, 0);
+
+BOOST_CLASS_VERSION(Kazv::ClientModel, 0)
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
index 356d960..d02e903 100644
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -1,123 +1,121 @@
/*
* Copyright (C) 2020-2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <lager/context.hpp>
#include <context.hpp>
-#include "error.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
using namespace Api;
class JobInterface;
class EventInterface;
struct LoginAction;
struct TokenLoginAction;
struct LogoutAction;
struct SyncAction;
struct PostInitialFiltersAction;
struct PaginateTimelineAction;
struct SendMessageAction;
struct SendStateEventAction;
struct CreateRoomAction;
struct GetRoomStatesAction;
struct GetStateEventAction;
struct InviteToRoomAction;
struct JoinRoomByIdAction;
struct EmitKazvEventsAction;
struct JoinRoomAction;
struct LeaveRoomAction;
struct ForgetRoomAction;
struct ProcessResponseAction;
struct SetTypingAction;
struct PostReceiptAction;
struct SetReadMarkerAction;
struct UploadContentAction;
struct DownloadContentAction;
struct DownloadThumbnailAction;
struct SendToDeviceMessageAction;
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
struct ClaimKeysAndSendSessionKeyAction;
struct ResubmitJobAction;
struct ClientModel;
using ClientAction = std::variant<
RoomListAction,
- Error::Action,
LoginAction,
TokenLoginAction,
LogoutAction,
SyncAction,
PostInitialFiltersAction,
PaginateTimelineAction,
SendMessageAction,
SendStateEventAction,
CreateRoomAction,
GetRoomStatesAction,
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
JoinRoomAction,
LeaveRoomAction,
ForgetRoomAction,
ProcessResponseAction,
SetTypingAction,
PostReceiptAction,
SetReadMarkerAction,
UploadContentAction,
DownloadContentAction,
DownloadThumbnailAction,
SendToDeviceMessageAction,
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
ClaimKeysAndSendSessionKeyAction,
ResubmitJobAction
>;
using ClientEffect = Effect<ClientAction, lager::deps<>>;
using ClientResult = std::pair<ClientModel, ClientEffect>;
}
diff --git a/src/client/clientutil.hpp b/src/client/clientutil.hpp
index 9b860d3..acd3f95 100644
--- a/src/client/clientutil.hpp
+++ b/src/client/clientutil.hpp
@@ -1,232 +1,235 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@kazv.moe>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <string>
#include <tuple>
#include <immer/map.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/transducer/eager.hpp>
#include <lager/deps.hpp>
#include <boost/container_hash/hash.hpp>
+#include <boost/serialization/string.hpp>
#include <cursorutil.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
namespace Kazv
{
template<class K, class V, class List, class Func>
immer::map<K, V> merge(immer::map<K, V> map, List list, Func keyOf)
{
for (auto v : list) {
auto key = keyOf(v);
map = std::move(map).set(key, v);
}
return map;
}
inline std::string keyOfPresence(Event e) {
return e.sender();
}
inline std::string keyOfAccountData(Event e) {
return e.type();
}
inline std::string keyOfTimeline(Event e) {
return e.id();
}
inline std::string keyOfEphemeral(Event e) {
return e.type();
}
struct KeyOfState {
std::string type;
std::string stateKey;
};
template<class Archive>
- void serialize(Archive &ar, KeyOfState &m)
+ void serialize(Archive &ar, KeyOfState &m, std::uint32_t const /* version */)
{
- ar(m.type, m.stateKey);
+ ar & m.type & m.stateKey;
}
inline bool operator==(KeyOfState a, KeyOfState b)
{
return a.type == b.type && a.stateKey == b.stateKey;
}
inline KeyOfState keyOfState(Event e) {
return {e.type(), e.stateKey()};
}
template<class Context>
JobInterface &getJobHandler(Context &&ctx)
{
return lager::get<JobInterface &>(std::forward<Context>(ctx));
}
template<class Context>
EventInterface &getEventEmitter(Context &&ctx)
{
return lager::get<EventInterface &>(std::forward<Context>(ctx));
}
namespace
{
template<class ImmerT>
struct ImmerIterator
{
using value_type = typename ImmerT::value_type;
using reference = typename ImmerT::reference;
using pointer = const value_type *;
using difference_type = long int;
using iterator_category = std::random_access_iterator_tag;
ImmerIterator(const ImmerT &container, std::size_t index)
: m_container(std::ref(container))
, m_index(index)
{}
ImmerIterator &operator+=(difference_type d) {
m_index += d;
return *this;
}
ImmerIterator &operator-=(difference_type d) {
m_index -= d;
return *this;
}
difference_type operator-(ImmerIterator b) const {
return index() - b.index();
}
ImmerIterator &operator++() {
return *this += 1;
}
ImmerIterator operator++(int) {
auto tmp = *this;
*this += 1;
return tmp;
}
ImmerIterator &operator--() {
return *this -= 1;
}
ImmerIterator operator--(int) {
auto tmp = *this;
*this -= 1;
return tmp;
}
reference &operator*() const {
return m_container.get().at(m_index);
}
reference operator[](difference_type d) const;
std::size_t index() const { return m_index; }
private:
std::reference_wrapper<const ImmerT> m_container;
std::size_t m_index;
};
template<class ImmerT>
auto ImmerIterator<ImmerT>::operator[](difference_type d) const -> reference
{
return *(*this + d);
}
template<class ImmerT>
auto operator+(ImmerIterator<ImmerT> a, long int d)
{
return a += d;
};
template<class ImmerT>
auto operator+(long int d, ImmerIterator<ImmerT> a)
{
return a += d;
};
template<class ImmerT>
auto operator-(ImmerIterator<ImmerT> a, long int d)
{
return a -= d;
};
template<class ImmerT>
auto immerBegin(const ImmerT &c)
{
return ImmerIterator<ImmerT>(c, 0);
}
template<class ImmerT>
auto immerEnd(const ImmerT &c)
{
return ImmerIterator<ImmerT>(c, c.size());
}
}
template<class ImmerT1, class RangeT2, class Pred, class Func>
ImmerT1 sortedUniqueMerge(ImmerT1 base, RangeT2 addon, Pred exists, Func keyOf)
{
auto needToAdd = intoImmer(ImmerT1{},
zug::filter([=](auto a) {
return !exists(a);
}),
addon);
auto cmp = [=](auto a, auto b) {
return keyOf(a) < keyOf(b);
};
for (auto item : needToAdd) {
auto it = std::upper_bound(immerBegin(base), immerEnd(base), item, cmp);
auto index = it.index();
base = std::move(base).insert(index, item);
}
return base;
}
}
namespace std
{
template<> struct hash<Kazv::KeyOfState>
{
std::size_t operator()(const Kazv::KeyOfState & k) const noexcept {
std::size_t seed = 0;
boost::hash_combine(seed, k.type);
boost::hash_combine(seed, k.stateKey);
return seed;
}
};
}
+
+BOOST_CLASS_VERSION(Kazv::KeyOfState, 0)
diff --git a/src/client/device-list-tracker.hpp b/src/client/device-list-tracker.hpp
index c53f420..f1a66b0 100644
--- a/src/client/device-list-tracker.hpp
+++ b/src/client/device-list-tracker.hpp
@@ -1,89 +1,119 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <string>
+
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
+#include <boost/serialization/string.hpp>
+#include <boost/serialization/optional.hpp>
+
+#include <serialization/immer-map.hpp>
+
#include <crypto.hpp>
#include <csapi/keys.hpp>
#include "cursorutil.hpp"
namespace Kazv
{
enum DeviceTrustLevel
{
Blocked,
Unseen,
Seen,
Verified,
};
struct DeviceKeyInfo
{
std::string deviceId;
std::string ed25519Key;
std::string curve25519Key;
std::optional<std::string> displayName;
DeviceTrustLevel trustLevel{Unseen};
};
bool operator==(DeviceKeyInfo a, DeviceKeyInfo b);
+ template<class Archive>
+ void serialize(Archive &ar, DeviceKeyInfo &i, std::uint32_t const /*version*/)
+ {
+ ar
+ & i.deviceId
+ & i.ed25519Key
+ & i.curve25519Key
+ & i.displayName
+ & i.trustLevel
+ ;
+ }
+
struct DeviceListTracker
{
using DeviceMapT = immer::map<std::string /* deviceId */, DeviceKeyInfo>;
immer::map<std::string /* userId */, bool /* outdated */> usersToTrackDeviceLists;
immer::map<std::string /* userId */, DeviceMapT> deviceLists;
template<class RangeT>
void track(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists)
.set(userId, true);
}
}
template<class RangeT>
void untrack(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists).erase(userId);
}
}
immer::flex_vector<std::string> outdatedUsers() const;
bool addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto);
void markUpToDate(std::string userId);
DeviceMapT devicesFor(std::string userId) const;
std::optional<DeviceKeyInfo> get(std::string userId, std::string deviceId) const;
std::optional<DeviceKeyInfo> findByEd25519Key(std::string userId, std::string ed25519Key) const;
std::optional<DeviceKeyInfo> findByCurve25519Key(std::string userId, std::string curve25519Key) const;
/// returns a list of users whose device list has changed
immer::flex_vector<std::string> diff(DeviceListTracker that) const;
};
+
+ template<class Archive>
+ void serialize(Archive &ar, DeviceListTracker &t, std::uint32_t const /*version*/)
+ {
+ ar
+ & t.usersToTrackDeviceLists
+ & t.deviceLists
+ ;
+ }
}
+
+BOOST_CLASS_VERSION(Kazv::DeviceKeyInfo, 0)
+BOOST_CLASS_VERSION(Kazv::DeviceListTracker, 0)
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index 9a9a458..3406e58 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,237 +1,254 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@kazv.moe>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <lager/debug/cereal/struct.hpp>
-#include <lager/debug/cereal/immer_flex_vector.hpp>
#include <string>
#include <variant>
#include <immer/flex_vector.hpp>
#include <immer/map.hpp>
+#include <serialization/immer-flex-vector.hpp>
+#include <serialization/immer-box.hpp>
+#include <serialization/immer-map.hpp>
+#include <serialization/immer-array.hpp>
+
#include <csapi/sync.hpp>
#include <event.hpp>
#include <crypto.hpp>
#include "data/cereal_map.hpp"
#include "clientutil.hpp"
namespace Kazv
{
struct AddStateEventsAction
{
immer::flex_vector<Event> stateEvents;
};
struct AppendTimelineAction
{
immer::flex_vector<Event> events;
};
struct PrependTimelineAction
{
immer::flex_vector<Event> events;
std::string paginateBackToken;
};
struct AddToTimelineAction
{
/// Events from oldest to latest
immer::flex_vector<Event> events;
std::optional<std::string> prevBatch;
std::optional<bool> limited;
std::optional<std::string> gapEventId;
};
struct AddAccountDataAction
{
immer::flex_vector<Event> events;
};
struct ChangeMembershipAction
{
RoomMembership membership;
};
struct ChangeInviteStateAction
{
immer::flex_vector<Event> events;
};
struct AddEphemeralAction
{
EventList events;
};
struct SetLocalDraftAction
{
std::string localDraft;
};
struct SetRoomEncryptionAction
{
};
struct MarkMembersFullyLoadedAction
{
};
struct RoomModel
{
using Membership = RoomMembership;
std::string roomId;
immer::map<KeyOfState, Event> stateEvents;
immer::map<KeyOfState, Event> inviteState;
// Smaller indices mean earlier events
// (oldest) 0 --------> n (latest)
immer::flex_vector<std::string> timeline;
immer::map<std::string, Event> messages;
immer::map<std::string, Event> accountData;
Membership membership{};
std::string paginateBackToken;
/// whether this room has earlier events to be fetched
bool canPaginateBack{true};
immer::map<std::string /* eventId */, std::string /* prevBatch */> timelineGaps;
immer::map<std::string, Event> ephemeral;
std::string localDraft;
bool encrypted{false};
/// a marker to indicate whether we need to rotate
/// the session key earlier than it expires
/// (e.g. when a user in the room's device list changed
/// or when someone joins or leaves)
bool shouldRotateSessionKey{true};
bool membersFullyLoaded{false};
immer::flex_vector<std::string> joinedMemberIds() const;
MegOlmSessionRotateDesc sessionRotateDesc() const;
bool hasUser(std::string userId) const;
using Action = std::variant<
AddStateEventsAction,
AppendTimelineAction,
PrependTimelineAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
SetLocalDraftAction,
SetRoomEncryptionAction,
MarkMembersFullyLoadedAction
>;
static RoomModel update(RoomModel r, Action a);
};
using RoomAction = RoomModel::Action;
inline bool operator==(RoomModel a, RoomModel b)
{
return a.roomId == b.roomId
&& a.stateEvents == b.stateEvents
&& a.inviteState == b.inviteState
&& a.timeline == b.timeline
&& a.messages == b.messages
&& a.accountData == b.accountData
&& a.membership == b.membership
&& a.paginateBackToken == b.paginateBackToken
&& a.canPaginateBack == b.canPaginateBack
&& a.timelineGaps == b.timelineGaps
&& a.ephemeral == b.ephemeral
&& a.localDraft == b.localDraft
&& a.encrypted == b.encrypted
&& a.shouldRotateSessionKey == b.shouldRotateSessionKey
&& a.membersFullyLoaded == b.membersFullyLoaded;
}
struct UpdateRoomAction
{
std::string roomId;
RoomAction roomAction;
};
struct RoomListModel
{
immer::map<std::string, RoomModel> rooms;
inline auto at(std::string id) const { return rooms.at(id); }
inline auto operator[](std::string id) const { return rooms[id]; }
inline bool has(std::string id) const { return rooms.find(id); }
using Action = std::variant<
UpdateRoomAction
>;
static RoomListModel update(RoomListModel l, Action a);
};
using RoomListAction = RoomListModel::Action;
inline bool operator==(RoomListModel a, RoomListModel b)
{
return a.rooms == b.rooms;
}
#ifndef NDEBUG
LAGER_CEREAL_STRUCT(AddStateEventsAction);
LAGER_CEREAL_STRUCT(AppendTimelineAction);
LAGER_CEREAL_STRUCT(PrependTimelineAction);
LAGER_CEREAL_STRUCT(AddAccountDataAction);
LAGER_CEREAL_STRUCT(ChangeMembershipAction);
LAGER_CEREAL_STRUCT(SetLocalDraftAction);
LAGER_CEREAL_STRUCT(ChangeInviteStateAction);
LAGER_CEREAL_STRUCT(UpdateRoomAction);
#endif
template<class Archive>
void serialize(Archive &ar, RoomModel &r, std::uint32_t const /*version*/)
{
- ar(r.roomId,
- r.stateEvents,
- r.inviteState,
- r.timeline,
- r.messages,
- r.accountData,
- r.membership,
- r.paginateBackToken,
- r.canPaginateBack);
+ ar
+ & r.roomId
+ & r.stateEvents
+ & r.inviteState
+
+ & r.timeline
+ & r.messages
+ & r.accountData
+ & r.membership
+ & r.paginateBackToken
+ & r.canPaginateBack
+
+ & r.timelineGaps
+ & r.ephemeral
+
+ & r.localDraft
+
+ & r.encrypted
+ & r.shouldRotateSessionKey
+
+ & r.membersFullyLoaded
+ ;
}
template<class Archive>
void serialize(Archive &ar, RoomListModel &l, std::uint32_t const /*version*/)
{
- ar(l.rooms);
+ ar & l.rooms;
}
}
-CEREAL_CLASS_VERSION(Kazv::RoomModel, 0);
-CEREAL_CLASS_VERSION(Kazv::RoomListModel, 0);
+BOOST_CLASS_VERSION(Kazv::RoomModel, 0)
+BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/sdk-model.hpp b/src/client/sdk-model.hpp
index 8663f33..3eec4c4 100644
--- a/src/client/sdk-model.hpp
+++ b/src/client/sdk-model.hpp
@@ -1,59 +1,59 @@
/*
* Copyright (C) 2020 Tusooa Zhu <tusooa@vista.aero>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <libkazv-config.hpp>
#include <context.hpp>
#include "client-model.hpp"
namespace Kazv
{
struct SdkModel;
using SdkAction = std::variant<
ClientAction
>;
using SdkEffect = Effect<SdkAction, lager::deps<JobInterface &, EventInterface &>>;
using SdkResult = std::pair<SdkModel, SdkEffect>;
struct SdkModel
{
ClientModel client;
inline operator ClientModel() const { return client; }
inline const ClientModel &c() const { return client; }
using Action = SdkAction;
using Effect = SdkEffect;
using Result = SdkResult;
static SdkResult update(SdkModel s, SdkAction a);
};
template<class Archive>
void serialize(Archive &ar, SdkModel &s, std::uint32_t const /*version*/)
{
- ar(s.client);
+ ar & s.client;
}
}
-CEREAL_CLASS_VERSION(Kazv::SdkModel, 0);
+BOOST_CLASS_VERSION(Kazv::SdkModel, 0)
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index a9b788e..1e041fc 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,42 +1,43 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
add_executable(kazvtest
testmain.cpp
basejobtest.cpp
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
client/client-test-util.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/util-test.cpp
+ client/serialization-test.cpp
kazvjobtest.cpp
event-emitter-test.cpp
crypto-test.cpp
promise-test.cpp
store-test.cpp
file-desc-test.cpp
)
target_include_directories(
kazvtest
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(kazvtest
PRIVATE Catch2::Catch2
PRIVATE kazv
PRIVATE kazveventemitter
PRIVATE kazvjob
PRIVATE nlohmann_json::nlohmann_json
PRIVATE immer
PRIVATE lager
PRIVATE zug)
diff --git a/src/tests/base/serialization-test.cpp b/src/tests/base/serialization-test.cpp
index 8c2a575..48d6377 100644
--- a/src/tests/base/serialization-test.cpp
+++ b/src/tests/base/serialization-test.cpp
@@ -1,115 +1,132 @@
/*
* Copyright (C) 2021 Tusooa Zhu <tusooa@kazv.moe>
*
* This file is part of libkazv.
*
* libkazv is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* libkazv 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with libkazv. If not, see <https://www.gnu.org/licenses/>.
*/
#include <libkazv-config.hpp>
#include <catch2/catch.hpp>
#include <sstream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-map.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-array.hpp>
+#include <serialization/std-optional.hpp>
#include <event.hpp>
using namespace Kazv;
using IAr = boost::archive::text_iarchive;
using OAr = boost::archive::text_oarchive;
template<class T>
void serializeTest(const T &in, T &out)
{
std::stringstream stream;
{
auto ar = OAr(stream);
ar << in;
}
{
auto ar = IAr(stream);
ar >> out;
}
REQUIRE(in == out);
}
TEST_CASE("Serialize immer::array", "[base][serialization]")
{
immer::array<int> v{1, 2, 3, 4, 5};
immer::array<int> v2{50};
serializeTest(v, v2);
}
TEST_CASE("Serialize immer::flex_vector", "[base][serialization]")
{
immer::flex_vector<int> v{1, 2, 3, 4, 5};
immer::flex_vector<int> v2{50};
serializeTest(v, v2);
}
TEST_CASE("Serialize immer::map", "[base][serialization]")
{
auto v = immer::map<int, int>{}
.set(1, 6)
.set(2, 7)
.set(3, 8)
.set(4, 9)
.set(5, 10);
auto v2 = immer::map<int, int>{}.set(10, 7);
serializeTest(v, v2);
}
TEST_CASE("Serialize immer::box", "[base][serialization]")
{
auto v = immer::box<int>{42};
immer::box<int> v2;
serializeTest(v, v2);
}
TEST_CASE("Serialize JsonWrap", "[base][serialization]")
{
auto v = JsonWrap{json::object({{"foo", "bar"}})};
auto v2 = JsonWrap{};
serializeTest(v, v2);
v = json::array({"mew"});
serializeTest(v, v2);
}
TEST_CASE("Serialize Event", "[base][serialization]")
{
auto v = Event{R"({
"type": "m.room.encrypted",
"content": {},
"sender": "@example:example.org",
"event_id": "!example:example.org"
})"_json};
auto v2 = Event{};
serializeTest(v, v2);
}
+
+TEST_CASE("Serialize std::optional", "[base][serialization]")
+{
+ std::optional<int> o{20};
+ std::optional<int> o2{1};
+
+ serializeTest(o, o2);
+
+ o2.reset();
+
+ serializeTest(o, o2);
+
+ o.reset();
+
+ serializeTest(o, o2);
+}
diff --git a/src/tests/client/serialization-test.cpp b/src/tests/client/serialization-test.cpp
new file mode 100644
index 0000000..5dc1157
--- /dev/null
+++ b/src/tests/client/serialization-test.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 Tusooa Zhu <tusooa@kazv.moe>
+ *
+ * This file is part of libkazv.
+ *
+ * libkazv is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * libkazv 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with libkazv. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch.hpp>
+
+#include <sstream>
+#include <boost/archive/text_iarchive.hpp>
+#include <boost/archive/text_oarchive.hpp>
+
+#include <sdk-model.hpp>
+
+using namespace Kazv;
+using IAr = boost::archive::text_iarchive;
+using OAr = boost::archive::text_oarchive;
+
+TEST_CASE("Serialize SdkModel", "[client][serialization]")
+{
+ SdkModel m1;
+ SdkModel m2;
+
+ std::stringstream stream;
+
+ {
+ auto ar = OAr(stream);
+ ar << m1;
+ }
+
+ {
+ auto ar = IAr(stream);
+ ar >> m2;
+ }
+
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 2:35 PM (19 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55230
Default Alt Text
(51 KB)

Event Timeline