1 /*
2  * edbflatfile.cpp - asynchronous I/O event database
3  * Copyright (C) 2001, 2002  Justin Karneges
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18  *
19  */
20 
21 #include <QVector>
22 #include <QFileInfo>
23 #include <QDir>
24 #include <QTimer>
25 #include <QTextStream>
26 #include <QDateTime>
27 
28 #include "edbflatfile.h"
29 #include "psicon.h"
30 #include "psiaccount.h"
31 #include "psicontactlist.h"
32 #include "xmpp_jid.h"
33 #include "jidutil.h"
34 #include "common.h"
35 #include "applicationinfo.h"
36 
37 #define FAKEDELAY 0
38 
39 using namespace XMPP;
40 
41 //----------------------------------------------------------------------------
42 // EDBFlatFile
43 //----------------------------------------------------------------------------
44 struct item_file_req
45 {
46 	Jid j;
47 	int type; // 0 = latest, 1 = oldest, 2 = random, 3 = write
48 	int start;
49 	int len;
50 	int dir;
51 	int id;
52 	QDateTime date;
53 	QString findStr;
54 	PsiEvent::Ptr event;
55 
56 	enum Type {
57 		Type_get,
58 		Type_append,
59 		Type_find,
60 		Type_erase
61 	};
62 };
63 
64 class EDBFlatFile::Private
65 {
66 public:
Private()67 	Private() {}
68 
69 	QList<File*> flist;
70 	QList<item_file_req*> rlist;
71 };
72 
EDBFlatFile(PsiCon * psi)73 EDBFlatFile::EDBFlatFile(PsiCon *psi)
74 	: EDB(psi)
75 {
76 	d = new Private;
77 }
78 
~EDBFlatFile()79 EDBFlatFile::~EDBFlatFile()
80 {
81 	qDeleteAll(d->rlist);
82 	qDeleteAll(d->flist);
83 	d->flist.clear();
84 
85 	delete d;
86 }
87 
features() const88 int EDBFlatFile::features() const
89 {
90 	return 0;
91 }
92 
get(const QString &,const Jid & j,const QDateTime date,int direction,int start,int len)93 int EDBFlatFile::get(const QString &/*accId*/, const Jid &j, const QDateTime date, int direction, int start, int len)
94 {
95 	item_file_req *r = new item_file_req;
96 	r->j = j;
97 	r->type = item_file_req::Type_get;
98 	r->start = start;
99 	r->len = len < 1 ? 1: len;
100 	r->dir = direction;
101 	r->date = date;
102 	r->id = genUniqueId();
103 	d->rlist.append(r);
104 
105 	QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
106 	return r->id;
107 }
108 
find(const QString &,const QString & str,const Jid & j,const QDateTime date,int direction)109 int EDBFlatFile::find(const QString &/*accId*/, const QString &str, const Jid &j, const QDateTime date, int direction)
110 {
111 	item_file_req *r = new item_file_req;
112 	r->j = j;
113 	r->type = item_file_req::Type_find;
114 	r->len = 1;
115 	r->dir = direction;
116 	r->findStr = str;
117 	r->date = date;
118 	r->id = genUniqueId();
119 	d->rlist.append(r);
120 
121 	QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
122 	return r->id;
123 }
124 
append(const QString &,const Jid & j,const PsiEvent::Ptr & e,int type)125 int EDBFlatFile::append(const QString &/*accId*/, const Jid &j, const PsiEvent::Ptr &e, int type)
126 {
127 	if (type != EDB::Contact)
128 		return 0;
129 	item_file_req *r = new item_file_req;
130 	r->j = j;
131 	r->type = item_file_req::Type_append;
132 	r->event = e;
133 	if ( !r->event ) {
134 		qWarning("EDBFlatFile::append(): Attempted to append incompatible type.");
135 		delete r;
136 		return 0;
137 	}
138 	r->id = genUniqueId();
139 	d->rlist.append(r);
140 
141 	QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
142 	return r->id;
143 }
144 
erase(const QString &,const Jid & j)145 int EDBFlatFile::erase(const QString &/*accId*/, const Jid &j)
146 {
147 	item_file_req *r = new item_file_req;
148 	r->j = j;
149 	r->type = item_file_req::Type_erase;
150 	r->id = genUniqueId();
151 	d->rlist.append(r);
152 
153 	QTimer::singleShot(FAKEDELAY, this, SLOT(performRequests()));
154 	return r->id;
155 }
156 
contacts(const QString & accId,int type)157 QList<EDB::ContactItem> EDBFlatFile::contacts(const QString &accId, int type)
158 {
159 	if (!accId.isEmpty())
160 		return File::contacts(accId, type);
161 	return File::contacts(psi()->contactList()->defaultAccount()->id(), type);
162 }
163 
eventsCount(const QString & accId,const XMPP::Jid & jid)164 quint64 EDBFlatFile::eventsCount(const QString &accId, const XMPP::Jid &jid)
165 {
166 	quint64 res = 0;
167 	if (!jid.isEmpty())
168 		res = ensureFile(jid)->total();
169 	else
170 		foreach (const ContactItem &ci, contacts(accId, Contact))
171 			res += ensureFile(ci.jid)->total();
172 	return res;
173 }
174 
findFile(const Jid & j) const175 EDBFlatFile::File *EDBFlatFile::findFile(const Jid &j) const
176 {
177 	foreach(File* i, d->flist) {
178 		if(i->j.compare(j, false))
179 			return i;
180 	}
181 	return 0;
182 }
183 
ensureFile(const Jid & j)184 EDBFlatFile::File *EDBFlatFile::ensureFile(const Jid &j)
185 {
186 	File *i = findFile(j);
187 	if(!i) {
188 		i = new File(Jid(j.bare()));
189 		connect(i, SIGNAL(timeout()), SLOT(file_timeout()));
190 		d->flist.append(i);
191 	}
192 	return i;
193 }
194 
deleteFile(const Jid & j)195 bool EDBFlatFile::deleteFile(const Jid &j)
196 {
197 	File *i = findFile(j);
198 
199 	QString fname;
200 
201 	if (i) {
202 		fname = i->fname;
203 		d->flist.removeAll(i);
204 		delete i;
205 	}
206 	else {
207 		fname = File::jidToFileName(j);
208 	}
209 
210 	QFileInfo fi(fname);
211 	if(fi.exists()) {
212 		QDir dir = fi.dir();
213 		return dir.remove(fi.fileName());
214 	}
215 	else
216 		return true;
217 }
218 
performRequests()219 void EDBFlatFile::performRequests()
220 {
221 	if(d->rlist.isEmpty())
222 		return;
223 
224 	item_file_req *r = d->rlist.takeFirst();
225 
226 	File *f = ensureFile(r->j);
227 	int type = r->type;
228 	if(type == item_file_req::Type_get) {
229 		EDBResult result;
230 		int startId = 0;
231 		int direction = r->dir;
232 		int id = f->getId(r->date, direction, r->start);
233 		if (id != -1) {
234 			int len;
235 			if(direction == Forward) {
236 				if(id + r->len > f->total())
237 					len = f->total() - id;
238 				else
239 					len = r->len;
240 			}
241 			else {
242 				if((id+1) - r->len < 0)
243 					len = id+1;
244 				else
245 					len = r->len;
246 			}
247 
248 			startId = id;
249 			for(int n = 0; n < len; ++n) {
250 				PsiEvent::Ptr e(f->get(id));
251 				if(e) {
252 					EDBItemPtr ei = EDBItemPtr(new EDBItem(e, QString::number(id)));
253 					result.append(ei);
254 				}
255 
256 				if(direction == Forward)
257 					++id;
258 				else
259 					--id;
260 			}
261 			if (direction == Backward)
262 				startId = id + 1;
263 		}
264 		resultReady(r->id, result, startId);
265 	}
266 	else if(type == item_file_req::Type_append) {
267 		writeFinished(r->id, f->append(r->event));
268 	}
269 	else if(type == item_file_req::Type_find) {
270 		int id = f->getId(r->date, r->dir, 0);
271 		EDBResult result;
272 		if (id != -1) {
273 			while (1) {
274 				PsiEvent::Ptr e(f->get(id));
275 				if (!e)
276 					break;
277 
278 				if(e->type() == PsiEvent::Message) {
279 					MessageEvent::Ptr me = e.staticCast<MessageEvent>();
280 					const Message &m = me->message();
281 					if(m.body().indexOf(r->findStr, 0, Qt::CaseInsensitive) != -1) {
282 						EDBItemPtr ei = EDBItemPtr(new EDBItem(e, QString::number(id)));
283 						result.append(ei);
284 						//commented line below to return ALL(instead of just first) messages that contain findStr
285 						//break;
286 					}
287 				}
288 				if(r->dir == Forward)
289 					++id;
290 				else
291 					--id;
292 			}
293 		}
294 		resultReady(r->id, result, 0);
295 	}
296 	else if(type == item_file_req::Type_erase) {
297 		writeFinished(r->id, deleteFile(f->j));
298 	}
299 	else {
300 		qWarning("EDBFlatFile::performRequests(): Invalid type.");
301 	}
302 
303 	delete r;
304 }
305 
file_timeout()306 void EDBFlatFile::file_timeout()
307 {
308 	File *i = (File *)sender();
309 	d->flist.removeAll(i);
310 	i->deleteLater();
311 }
312 
313 
314 //----------------------------------------------------------------------------
315 // EDBFlatFile::File
316 //----------------------------------------------------------------------------
317 class EDBFlatFile::File::Private
318 {
319 public:
Private()320 	Private() {}
321 
322 	QVector<quint64> index;
323 	bool indexed;
324 };
325 
File(const Jid & _j)326 EDBFlatFile::File::File(const Jid &_j)
327 {
328 	d = new Private;
329 	d->indexed = false;
330 
331 	j = _j;
332 	valid = false;
333 	t = new QTimer(this);
334 	connect(t, SIGNAL(timeout()), SLOT(timer_timeout()));
335 
336 	//printf("[EDB opening -- %s]\n", j.full().latin1());
337 	fname = jidToFileName(_j);
338 	f.setFileName(fname);
339 	valid = f.open(QIODevice::ReadWrite);
340 
341 	touch();
342 }
343 
~File()344 EDBFlatFile::File::~File()
345 {
346 	if(valid)
347 		f.close();
348 	//printf("[EDB closing -- %s]\n", j.full().latin1());
349 
350 	delete d;
351 }
352 
jidToFileName(const XMPP::Jid & j)353 QString EDBFlatFile::File::jidToFileName(const XMPP::Jid &j)
354 {
355 	return ApplicationInfo::historyDir() + "/" + strToFileName(JIDUtil::encode(j.bare()).toLower());
356 }
357 
strToFileName(const QString & s)358 QString EDBFlatFile::File::strToFileName(const QString &s)
359 {
360 	QFileInfo fi(s);
361 	return fi.fileName() + ".history";
362 }
363 
contacts(const QString & accId,int type)364 QList<EDB::ContactItem> EDBFlatFile::File::contacts(const QString &accId, int type)
365 {
366 	QList<ContactItem> res;
367 	if (type == EDB::Contact) {
368 		QDir dir(ApplicationInfo::historyDir() + "/");
369 		QFileInfoList flist = dir.entryInfoList(QStringList(strToFileName("*")), QDir::Files);
370 		foreach (const QFileInfo &fi, flist) {
371 			XMPP::Jid jid(JIDUtil::decode(fi.completeBaseName()));
372 			if (jid.isValid())
373 				res.append(ContactItem(accId, jid));
374 		}
375 	}
376 	return res;
377 }
378 
ensureIndex()379 void EDBFlatFile::File::ensureIndex()
380 {
381 	if ( valid && !d->indexed ) {
382 		if (f.isSequential()) {
383 			qWarning("EDBFlatFile::File::ensureIndex(): Can't index sequential files.");
384 			return;
385 		}
386 
387 		f.reset(); // go to beginning
388 		d->index.clear();
389 
390 		//printf(" file: %s\n", fname.latin1());
391 		// build index
392 		while(1) {
393 			quint64 at = f.pos();
394 
395 			// locate a newline
396 			bool found = false;
397 			char c;
398 			while (f.getChar(&c)) {
399 				if (c == '\n') {
400 					found = true;
401 					break;
402 				}
403 			}
404 
405 			if(!found)
406 				break;
407 
408 			int oldsize = d->index.size();
409 			d->index.resize(oldsize+1);
410 			d->index[oldsize] = at;
411 		}
412 
413 		d->indexed = true;
414 	}
415 	else {
416 		//printf(" file: can't open\n");
417 	}
418 
419 	//printf(" messages: %d\n\n", d->index.size());
420 }
421 
total() const422 int EDBFlatFile::File::total() const
423 {
424 	((EDBFlatFile::File *)this)->ensureIndex();
425 	return d->index.size();
426 }
427 
getId(QDateTime & date,int dir,int offset)428 int EDBFlatFile::File::getId(QDateTime &date, int dir, int offset)
429 {
430 	if (date.isNull()) {
431 		if (dir == EDBFlatFile::Forward)
432 			return offset;
433 		if (offset >= total())
434 			return 0;
435 		return total() - offset - 1;
436 	}
437 	ensureIndex();
438 	if (total() == 0)
439 		return 0;
440 	int id = findNearestDate(date);
441 	if (id == -1)
442 		return -1;
443 
444 	QDateTime fDate = getDate(id);
445 	if (!fDate.isValid())
446 		return -1;
447 
448 	if (dir == EDBFlatFile::Forward) {
449 		if (fDate < date)
450 			++id;
451 		id += offset;
452 	}
453 	else {
454 		if (fDate > date)
455 			--id;
456 		id -= offset;
457 	}
458 	if (id >= total())
459 		id = total() - 1;
460 	else if (id < 0)
461 		id = 0;
462 	return id;
463 }
464 
465 /*
466  * This method returns an index of a string with the event
467  * which has the nearest date to the specified one.
468  * Returned date may be earlier than that is passed as an argument.
469  */
findNearestDate(const QDateTime & date)470 int EDBFlatFile::File::findNearestDate(const QDateTime &date)
471 {
472 	int cnt = total();
473 	if (cnt == 0)
474 		return 0;
475 
476 	// Binary search algorithm
477 	int left  = 0;
478 	int right = cnt;
479 	while (right - left > 0) {
480 		int idx = left + (right - left) / 2;
481 		const QDateTime mid = getDate(idx);
482 		if (!mid.isValid())
483 			return -1;
484 		if (date <= mid)
485 			right = idx;
486 		else
487 			left = idx + 1;
488 	}
489 	// --
490 	if (right == cnt) // Specified date is later than the latest one in the history
491 		return cnt - 1;
492 
493 	// Now `right` is pointing to the index with an identical or later date
494 	while (right > 0) { // in case of there are more than one identical date
495 		const QDateTime dt = getDate(right - 1);
496 		if (!dt.isValid())
497 			return -1;
498 		if (dt != date)
499 			break;
500 		--right;
501 	}
502 	if (right == 0)
503 		return 0;
504 
505 	const QDateTime dt1 = getDate(right - 1);
506 	const QDateTime dt2 = getDate(right);
507 	if (!dt1.isValid() || !dt2.isValid())
508 		return -1;
509 	if (dt1.secsTo(date) <= date.secsTo(dt2)) // compares with earlier one
510 		--right;
511 	return right;
512 }
513 
touch()514 void EDBFlatFile::File::touch()
515 {
516 	t->start(30000);
517 }
518 
timer_timeout()519 void EDBFlatFile::File::timer_timeout()
520 {
521 	timeout();
522 }
523 
get(int id)524 PsiEvent::Ptr EDBFlatFile::File::get(int id)
525 {
526 	QString line = getLine(id);
527 	if (line.isNull())
528 		return PsiEvent::Ptr();
529 	PsiEvent::Ptr res = lineToEvent(line);
530 	if (!res)
531 		qWarning("EDBFlatFile::File::get() Failed to parse file %s, line %d", fname.toLatin1().data(), id + 1);
532 	return res;
533 }
534 
append(const PsiEvent::Ptr & e)535 bool EDBFlatFile::File::append(const PsiEvent::Ptr &e)
536 {
537 	touch();
538 
539 	if(!valid)
540 		return false;
541 
542 	QString line = eventToLine(e);
543 	if(line.isEmpty())
544 		return false;
545 
546 	f.seek(f.size());
547 	quint64 at = f.pos();
548 
549 	QTextStream t;
550 	t.setDevice(&f);
551 	t.setCodec("UTF-8");
552 	t << line << endl;
553 	f.flush();
554 
555 	if ( d->indexed ) {
556 		int oldsize = d->index.size();
557 		d->index.resize(oldsize+1);
558 		d->index[oldsize] = at;
559 	}
560 
561 	return true;
562 }
563 
lineToEvent(const QString & line)564 PsiEvent::Ptr EDBFlatFile::File::lineToEvent(const QString &line)
565 {
566 	// -- parse the line --
567 	enum { Time = 0, Type = 1, Origin = 2, Flags = 3, Subj = 4, UrlAddr = 5, UrlDesc = 6 };
568 	QStringList strData;
569 	int x1  = line.indexOf('|');
570 	if (x1 != -1) {
571 		++x1;
572 		for (int i = 0; i <= UrlDesc; ++i) // Filing default data
573 			strData << QString();
574 		int max = Flags;
575 		for (int idx = 0; idx <= max; ) {
576 			int x2 = line.indexOf('|', x1);
577 			if (x2 == -1) {
578 				x1 = -1;
579 				break;
580 			}
581 			QString s = line.mid(x1, x2 - x1);
582 			strData[idx] = s;
583 			x1 = x2 + 1;
584 
585 			if (idx == Flags) { // check for extra fields
586 				if (s.length() < 2) {
587 					x1 = -1;
588 					break;
589 				}
590 				if (s.at(1) != '-') {
591 					int subflag = QString(s.at(1)).toInt(NULL, 16);
592 					if (subflag & 1) // have subject?
593 						max = Subj;
594 					else // Skip subject
595 						++idx;
596 					if (subflag & 2) // have url?
597 						max = UrlDesc;
598 				}
599 			}
600 			++idx;
601 		}
602 	}
603 
604 	if (x1 == -1)
605 		return PsiEvent::Ptr();
606 
607 	// body text is last
608 	QString sText = line.mid(x1);
609 
610 	// -- read end --
611 
612 	int type = strData.at(Type).toInt();
613 	if(type == 0 || type == 1 || type == 4 || type == 5) {
614 		Message m;
615 		m.setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
616 		if(type == 1)
617 			m.setType("chat");
618 		else if(type == 4)
619 			m.setType("error");
620 		else if(type == 5)
621 			m.setType("headline");
622 		else
623 			m.setType("");
624 
625 		bool originLocal = (strData.at(Origin) == "to") ? true: false;
626 		m.setFrom(j);
627 		if (strData.at(Flags).at(0) == 'N')
628 			m.setBody(logdecode(sText));
629 		else
630 			m.setBody(logdecode(QString::fromUtf8(sText.toLatin1())));
631 		m.setSubject(logdecode(strData.at(Subj)));
632 
633 		QString url = logdecode(strData.at(UrlAddr));
634 		if(!url.isEmpty())
635 			m.urlAdd(Url(url, logdecode(strData.at(UrlDesc))));
636 		m.setSpooled(true);
637 
638 		MessageEvent::Ptr me(new MessageEvent(m, 0));
639 		me->setOriginLocal(originLocal);
640 
641 		return me.staticCast<PsiEvent>();
642 	}
643 	else if(type == 2 || type == 3 || type == 6 || type == 7 || type == 8) {
644 		QString subType = "subscribe";
645 		if(type == 2) {
646 			// stupid "system message" from Psi <= 0.8.6
647 			// try to figure out what kind it REALLY is based on the text
648 			if(sText == tr("<big>[System Message]</big><br>You are now authorized."))
649 				subType = "subscribed";
650 			else if(sText == tr("<big>[System Message]</big><br>Your authorization has been removed!"))
651 				subType = "unsubscribed";
652 		}
653 		else if(type == 3)
654 			subType = "subscribe";
655 		else if(type == 6)
656 			subType = "subscribed";
657 		else if(type == 7)
658 			subType = "unsubscribe";
659 		else if(type == 8)
660 			subType = "unsubscribed";
661 
662 		AuthEvent::Ptr ae(new AuthEvent(j, subType, 0));
663 		ae->setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate));
664 		return ae.staticCast<PsiEvent>();
665 	}
666 
667 	return PsiEvent::Ptr();
668 }
669 
eventToLine(const PsiEvent::Ptr & e)670 QString EDBFlatFile::File::eventToLine(const PsiEvent::Ptr &e)
671 {
672 	int subflags = 0;
673 	QString sTime, sType, sOrigin, sFlags;
674 
675 	if(e->type() == PsiEvent::Message) {
676 		MessageEvent::Ptr me = e.staticCast<MessageEvent>();
677 		const Message &m = me->message();
678 		const UrlList urls = m.urlList();
679 
680 		if(!m.subject().isEmpty())
681 			subflags |= 1;
682 		if(!urls.isEmpty())
683 			subflags |= 2;
684 
685 		sTime = m.timeStamp().toString(Qt::ISODate);
686 		int n = 0;
687 		if(m.type() == "chat")
688 			n = 1;
689 		else if(m.type() == "error")
690 			n = 4;
691 		else if(m.type() == "headline")
692 			n = 5;
693 		sType.setNum(n);
694 		sOrigin = e->originLocal() ? "to": "from";
695 		sFlags = "N---";
696 
697 		if(subflags != 0)
698 			sFlags[1] = QString::number(subflags,16)[0];
699 
700 		//  | date | type | To/from | flags | text
701 		QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";
702 
703 		if(subflags & 1) {
704 			line += logencode(m.subject()) + "|";
705 		}
706 		if(subflags & 2) {
707 			const Url &url = urls.first();
708 			line += logencode(url.url()) + "|";
709 			line += logencode(url.desc()) + "|";
710 		}
711 		line += logencode(m.body());
712 
713 		return line;
714 	}
715 	else if(e->type() == PsiEvent::Auth) {
716 		AuthEvent::Ptr ae = e.staticCast<AuthEvent>();
717 		sTime = ae->timeStamp().toString(Qt::ISODate);
718 		QString subType = ae->authType();
719 		int n = 0;
720 		if(subType == "subscribe")
721 			n = 3;
722 		else if(subType == "subscribed")
723 			n = 6;
724 		else if(subType == "unsubscribe")
725 			n = 7;
726 		else if(subType == "unsubscribed")
727 			n = 8;
728 		sType.setNum(n);
729 		sOrigin = e->originLocal() ? "to": "from";
730 		sFlags = "N---";
731 
732 		//  | date | type | To/from | flags | text
733 		QString line = "|" + sTime + "|" + sType + "|" + sOrigin + "|" + sFlags + "|";
734 		line += logencode(subType);
735 
736 		return line;
737 	}
738 
739 	return "";
740 }
741 
getLine(int id)742 QString EDBFlatFile::File::getLine(int id)
743 {
744 	touch();
745 
746 	if(!valid)
747 		return QString();
748 
749 	ensureIndex();
750 	if(id < 0 || id >= (int)d->index.size())
751 		return QString();
752 
753 	f.seek(d->index[id]);
754 
755 	QTextStream t;
756 	t.setDevice(&f);
757 	t.setCodec("UTF-8");
758 	return t.readLine();
759 }
760 
getDate(int id)761 QDateTime EDBFlatFile::File::getDate(int id)
762 {
763 	QString line = getLine(id);
764 	if (line.isNull())
765 		return QDateTime();
766 
767 	int p1 = line.indexOf('|') + 1;
768 	if (p1 == -1)
769 		return QDateTime();
770 	int p2 = line.indexOf('|', p1);
771 	if (p2 == -1)
772 		return QDateTime();
773 
774 	QString sTime = line.mid(p1, p2 - p1);
775 	return QDateTime::fromString(sTime, Qt::ISODate);
776 }
777