1 /*
2    Drawpile - a collaborative drawing program.
3 
4    Copyright (C) 2014-2019 Calle Laakkonen
5 
6    Drawpile is free software: you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation, either version 3 of the License, or
9    (at your option) any later version.
10 
11    Drawpile is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15 
16    You should have received a copy of the GNU General Public License
17    along with Drawpile.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "filter.h"
21 
22 #include "../libshared/record/reader.h"
23 #include "../libshared/record/writer.h"
24 #include "../libshared/net/undo.h"
25 #include "../libshared/net/brushes.h"
26 #include "../libshared/net/recording.h"
27 
28 #include <QDebug>
29 #include <QVector>
30 #include <QHash>
31 #include <QSet>
32 
33 namespace recording {
34 
35 namespace {
36 
37 static const uchar UNDOABLE = (1<<7);
38 static const uchar REMOVED = (1<<6);
39 
40 struct FilterIndex {
41 	// message type (protocol)
42 	uchar type;
43 
44 	// message context ID
45 	uchar ctxid;
46 
47 	// Filtering flags
48 	uchar flags;
49 
50 	// Message position in the recording file
51 	qint64 offset;
52 };
53 
54 struct State {
55 	// A FilterIndex entry for each message in the recording
56 	QVector<FilterIndex> index;
57 
58 	// User join/leave message indices
59 	// (used for removing looky-loos)
60 	QHash<int, QList<int>> userjoins;
61 
62 	// Replacement messages for specific indices
63 	// (currently used just for squishing)
64 	QHash<int, protocol::NullableMessageRef> replacements;
65 
66 	// List of users who have participated in the session somehow
67 	// (other than just loggin in/out)
68 	QSet<int> users_seen;
69 };
70 
mark_delete(FilterIndex & i)71 static inline void mark_delete(FilterIndex &i) {
72 	i.flags |= REMOVED;
73 }
74 
undostate(const FilterIndex & i)75 static inline protocol::MessageUndoState undostate(const FilterIndex &i) {
76 	return protocol::MessageUndoState(i.flags & 0x03);
77 }
78 
isUndoable(const FilterIndex & i)79 static inline bool isUndoable(const FilterIndex &i) {
80 	return i.flags & UNDOABLE;
81 }
82 
isDeleted(const FilterIndex & i)83 static inline bool isDeleted(const FilterIndex &i) {
84 	return i.flags & (REMOVED | protocol::UNDONE);
85 }
86 
87 // Basic filtering (including Undo)
filterMessage(const FilterOptions & options,State & state,protocol::MessagePtr msg,qint64 offset)88 static void filterMessage(const FilterOptions &options, State &state, protocol::MessagePtr msg, qint64 offset)
89 {
90 	// Put this message in the index
91 	state.index.append(FilterIndex {
92 		uchar(msg->type()),
93 		msg->contextId(),
94 		msg->isUndoable() ? UNDOABLE : uchar(0),
95 		offset
96 	});
97 
98 	// Filter out select message types
99 	switch(msg->type()) {
100 	using namespace protocol;
101 	case MSG_CHAT:
102 		if(options.removeChat) {
103 			mark_delete(state.index.last());
104 			return;
105 		}
106 		break;
107 
108 	case MSG_USER_JOIN:
109 	case MSG_USER_LEAVE:
110 		state.userjoins[msg->contextId()].append(state.index.size()-1);
111 		return;
112 
113 	case MSG_INTERVAL:
114 		if(options.removeDelays) {
115 			mark_delete(state.index.last());
116 			return;
117 		}
118 		break;
119 
120 	case MSG_LASERTRAIL:
121 	case MSG_MOVEPOINTER:
122 		if(options.removeLasers) {
123 			mark_delete(state.index.last());
124 			return;
125 		}
126 		break;
127 
128 	case MSG_MARKER:
129 		if(options.removeMarkers) {
130 			mark_delete(state.index.last());
131 			return;
132 		}
133 		break;
134 
135 	case MSG_FILTERED:
136 		// Always filter out FILTERED messages
137 		mark_delete(state.index.last());
138 		return;
139 
140 	default: break;
141 	}
142 
143 	// Perform undo
144 	if(msg->type() == protocol::MSG_UNDO && options.removeUndone) {
145 		// Normally, performing an undo will implicitly delete the undo action itself,
146 		// but it can be restored by a redo. Therefore, we must explicitly flag the
147 		// undo messages for deletion to be sure they are gone.
148 		mark_delete(state.index.last());
149 
150 		// Perform an undo. This is a stripped down version of handleUndo from StateTracker
151 		const protocol::Undo &cmd = msg.cast<protocol::Undo>();
152 
153 		const uchar ctxid = cmd.contextId();
154 
155 		// Step 1. Find undo or redo point
156 		int pos = state.index.size();
157 		int upCount = 0;
158 
159 		if(cmd.isRedo()) {
160 			// Find the start of the undo sequence (oldest undone UndoPoint)
161 			int redostart = pos;
162 			while(--pos>=0 && upCount <= protocol::UNDO_DEPTH_LIMIT) {
163 				const FilterIndex u = state.index[pos];
164 				if(u.type == protocol::MSG_UNDOPOINT) {
165 					++upCount;
166 					if(u.ctxid == ctxid) {
167 						if(undostate(u) != protocol::DONE)
168 							redostart = pos;
169 						else
170 							break;
171 					}
172 				}
173 			}
174 
175 			if(redostart == state.index.size()) {
176 				qDebug() << "nothing to redo for user" << cmd.contextId();
177 				mark_delete(state.index.last());
178 				return;
179 			}
180 			pos = redostart;
181 
182 		} else {
183 			// Search for undoable actions from the end of the
184 			// command stream towards the beginning
185 			while(--pos>=0 && upCount <= protocol::UNDO_DEPTH_LIMIT) {
186 				const FilterIndex u = state.index[pos];
187 
188 				if(u.type == protocol::MSG_UNDOPOINT) {
189 					++upCount;
190 					if(u.ctxid == ctxid && undostate(u) == protocol::DONE)
191 						break;
192 				}
193 			}
194 		}
195 
196 		if(upCount > protocol::UNDO_DEPTH_LIMIT) {
197 			qDebug() << "user" << cmd.contextId() << "cannot undo/redo beyond history limit";
198 			mark_delete(state.index.last());
199 			return;
200 		}
201 
202 		// Step 2 is not needed here
203 
204 		// Step 3. (Un)mark all actions by the user as undone
205 		if(cmd.isRedo()) {
206 			int i=pos;
207 			int sequence=2;
208 			while(i<state.index.size()) {
209 				FilterIndex &u = state.index[i];
210 				if(u.ctxid == ctxid) {
211 					if(u.type == protocol::MSG_UNDOPOINT && undostate(u) != protocol::GONE)
212 						if(--sequence==0)
213 							break;
214 
215 					// GONE messages cannot be redone
216 					if(undostate(u) == protocol::UNDONE)
217 						u.flags &= ~protocol::UNDONE;
218 				}
219 				++i;
220 			}
221 		} else {
222 			for(int i=pos;i<state.index.size();++i) {
223 				FilterIndex &u = state.index[i];
224 				if(u.ctxid == ctxid && isUndoable(u))
225 					u.flags |= protocol::UNDONE;
226 			}
227 		}
228 		// Steps 4 is not needed here.
229 	}
230 
231 	if(msg->contextId()>0)
232 		state.users_seen.insert(msg->contextId());
233 
234 	return;
235 }
236 
237 // Remove Join/Leave messages of those users who didn't do anything
238 // TODO this could be smarter. A user can log in&out many times, but
239 // not draw something every time. Perhaps this could preseve the
240 // outermost login/logout messages in that case?
filterLookyloos(State & state)241 static void filterLookyloos(State &state)
242 {
243 	for(const int id : state.userjoins.keys()) {
244 		if(!state.users_seen.contains(id)) {
245 			for(const int idx : state.userjoins[id]) {
246 				mark_delete(state.index[idx]);
247 			}
248 		}
249 	}
250 }
251 
252 /**
253  * @brief Remove adjacent undo points by the same user
254  *
255  * This removes all duplicate undo points that appear _next to each other_ in
256  * the message stream. Such undo points are left over when actions are undone or
257  * silenced.
258  *
259  * @param state
260  */
filterAdjacentUndoPoints(State & state)261 static void filterAdjacentUndoPoints(State &state)
262 {
263 	FilterIndex prev {0, 0, 0, 0};
264 
265 	for(int i=0;i<state.index.size();++i) {
266 		FilterIndex &fi = state.index[i];
267 
268 		if(isDeleted(fi) || fi.type == protocol::MSG_INTERVAL)
269 			continue;
270 
271 		if(
272 			fi.type == protocol::MSG_UNDOPOINT && prev.type == protocol::MSG_UNDOPOINT &&
273 			fi.ctxid == prev.ctxid
274 		) {
275 			mark_delete(fi);
276 		}
277 
278 		prev = fi;
279 	}
280 }
281 
isDabMessage(uchar msgtype)282 static inline bool isDabMessage(uchar msgtype) {
283 	return msgtype == protocol::MSG_DRAWDABS_CLASSIC ||
284 		msgtype == protocol::MSG_DRAWDABS_PIXEL ||
285 		msgtype == protocol::MSG_DRAWDABS_PIXEL_SQUARE
286 		;
287 
288 }
289 
squishStrokes(State & state,Reader & recording,QString * errorMessage)290 static bool squishStrokes(State &state, Reader &recording, QString *errorMessage)
291 {
292 	protocol::NullableMessageRef sequenceStartMessage;
293 	FilterIndex sequenceStart {0, 0, 0, 0};
294 	int sequenceStartIndex = 0;
295 
296 	for(int i=0;i<state.index.size();++i) {
297 		FilterIndex &fi = state.index[i];
298 
299 		if(isDeleted(fi))
300 			continue;
301 
302 		// Encountering any non-dab message ends a squishable sequence
303 		if(!isDabMessage(fi.type)) {
304 			sequenceStartMessage = nullptr;
305 			continue;
306 		}
307 
308 		// If we have an open sequence already, the new message type
309 		// and context ID must match
310 		if(!sequenceStartMessage.isNull()) {
311 			if(fi.type != sequenceStart.type || fi.ctxid != sequenceStart.ctxid) {
312 				sequenceStartMessage = nullptr;
313 				continue;
314 			}
315 		}
316 
317 		// At this point this message is either the start of a new sequence
318 		// OR a potential continuation of the current one
319 
320 		recording.seekTo(i, fi.offset);
321 		MessageRecord msgrecord = recording.readNext();
322 		if(msgrecord.status != MessageRecord::OK) {
323 			qWarning("Error reading message #%d at offset %lld", i, fi.offset);
324 			if(errorMessage)
325 				*errorMessage = "File read error";
326 			return false;
327 		}
328 
329 		if(!isDabMessage(msgrecord.message->type())) {
330 			qWarning("BUG: Inconsistency when reading recording. Expected a dab message at offset %lld, got %s", fi.offset, qPrintable(msgrecord.message->messageName()));
331 			if(errorMessage)
332 				*errorMessage = "Internal Application Error in squishStrokes function";
333 			return false;
334 		}
335 
336 		// If we have an open sequence, try squishing.
337 		if(!sequenceStartMessage.isNull() && sequenceStartMessage.cast<protocol::DrawDabs>().extend(msgrecord.message.cast<protocol::DrawDabs>())) {
338 
339 			// Squish succeeded. Store the extended message.
340 			if(!state.replacements.contains(sequenceStartIndex))
341 				state.replacements[sequenceStartIndex] = sequenceStartMessage;
342 
343 			mark_delete(fi);
344 
345 		} else {
346 			// Did not squish. This is a start of a new potentially squishable sequence
347 			sequenceStart = fi;
348 			sequenceStartMessage = msgrecord.message;
349 			sequenceStartIndex = i;
350 		}
351 	}
352 
353 	return true;
354 }
355 
doFilterRecording(const FilterOptions & options,State & state,Reader & recording,QString * errorMessage)356 static bool doFilterRecording(const FilterOptions &options, State &state, Reader &recording, QString *errorMessage)
357 {
358 	// Read input file and perform basic filtering
359 	// This constructs the filtering index
360 	while(true) {
361 		MessageRecord msg = recording.readNext();
362 		if(msg.status == MessageRecord::END_OF_RECORDING)
363 			break;
364 		if(msg.status == MessageRecord::INVALID) {
365 			qWarning() << "skipping invalid message type" << msg.invalid_type;
366 			continue;
367 		}
368 
369 		filterMessage(options, state, protocol::MessagePtr::fromNullable(msg.message), recording.currentPosition());
370 	}
371 
372 	// More complicated filtering that uses the index
373 
374 	if(options.removeLookyLoos)
375 		filterLookyloos(state);
376 
377 	if(options.squishStrokes) {
378 		if(!squishStrokes(state, recording, errorMessage))
379 			return false;
380 	}
381 
382 	filterAdjacentUndoPoints(state);
383 
384 	return true;
385 }
386 
387 }
388 
filterRecording(const QString & input,const QString & outputfile,const FilterOptions & options,QString * errorMessage)389 bool filterRecording(const QString &input, const QString &outputfile, const FilterOptions &options, QString *errorMessage)
390 {
391 	// Step 1. Open input and output files
392 	Reader reader(input);
393 	Compatibility readOk = reader.open();
394 	if(readOk != COMPATIBLE && readOk != MINOR_INCOMPATIBILITY) {
395 		qWarning() << "Cannot open recording for filtering. Error code" << readOk;
396 		if(errorMessage)
397 			*errorMessage = reader.errorString();
398 		return false;
399 	}
400 
401 	Writer writer(outputfile);
402 	if(!writer.open()) {
403 		qWarning() << "Cannot open record filter output file";
404 		if(errorMessage)
405 			*errorMessage = writer.errorString();
406 		return false;
407 	}
408 
409 	// Step 2. Mark messages for removal and replacement
410 	State state;
411 	if(!doFilterRecording(options, state, reader, errorMessage))
412 		return false;
413 
414 	// Step 3. Copy remaining messages to output file
415 	reader.rewind();
416 
417 	writer.writeHeader();
418 
419 	QByteArray buffer;
420 	while(reader.readNextToBuffer(buffer)) {
421 		const int pos = reader.currentIndex();
422 
423 		// Copy (or replace) original message, unless marked for deletion
424 		if(!isDeleted(state.index[pos])) {
425 
426 			if(state.replacements.contains(pos))
427 				writer.writeMessage(*state.replacements[pos]);
428 			else
429 				writer.writeFromBuffer(buffer);
430 		}
431 	}
432 
433 	writer.close();
434 
435 	return true;
436 }
437 
438 }
439