1 #include <QApplication>
2 #include <QTest>
3 #include <QFont>
4 #include <QFontInfo>
5 #include <QDateTime>
6 #include <QDir>
7 #include <QFile>
8 #include <QFileInfo>
9 #include <QFileDialog>
10 #include <QMap>
11 #include <QMultiMap>
12 #include <QDateTime>
13 #include <QMessageBox>
14 #include <QScrollBar>
15 #include <QChar>
16 
17 #include "collectionrebuilder.h"
18 #include "settings.h"
19 #include "options.h"
20 #include "unzip.h"
21 #include "zip.h"
22 #include "sevenzipfile.h"
23 #if defined(QMC2_LIBARCHIVE_ENABLED)
24 #include "archivefile.h"
25 #endif
26 #include "machinelist.h"
27 #include "macros.h"
28 #include "softwarelist.h"
29 
30 #if defined(QMC2_OS_WIN)
31 #include <windows.h>
32 #else
33 #include <unistd.h>
34 #endif
35 #include <sys/stat.h>
36 #include <sys/types.h>
37 
38 extern Settings *qmc2Config;
39 extern Options *qmc2Options;
40 extern MachineList *qmc2MachineList;
41 extern SoftwareList *qmc2SoftwareList;
42 
43 QHash<QString, QString> CollectionRebuilderThread::m_replacementHash;
44 QStringList CollectionRebuilderThread::m_fileTypes;
45 
CollectionRebuilder(ROMAlyzer * myROMAlyzer,QWidget * parent)46 CollectionRebuilder::CollectionRebuilder(ROMAlyzer *myROMAlyzer, QWidget *parent) :
47 	QWidget(parent),
48 	m_romAlyzer(myROMAlyzer)
49 {
50 	setupUi(this);
51 
52 	switch ( romAlyzer()->mode() ) {
53 		case QMC2_ROMALYZER_MODE_SOFTWARE:
54 			m_settingsKey = "SoftwareCollectionRebuilder";
55 			m_defaultSetEntity = "software";
56 			m_defaultRomEntity = "rom";
57 			m_defaultDiskEntity = "disk";
58 			checkBoxFilterExpressionSoftwareLists->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpressionSoftwareLists", false).toBool());
59 			comboBoxFilterSyntaxSoftwareLists->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntaxSoftwareLists", 0).toInt());
60 			comboBoxFilterTypeSoftwareLists->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterTypeSoftwareLists", 0).toInt());
61 			toolButtonExactMatchSoftwareLists->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatchSoftwareLists", false).toBool());
62 			lineEditFilterExpressionSoftwareLists->setText(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpressionSoftwareLists", QString()).toString());
63 			checkBoxFilterExpression->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpression", false).toBool());
64 			comboBoxFilterSyntax->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntax", 0).toInt());
65 			comboBoxFilterType->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterType", 0).toInt());
66 			toolButtonExactMatch->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatch", false).toBool());
67 			lineEditFilterExpression->setText(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpression", QString()).toString());
68 			m_correctIconPixmap = QPixmap(QString::fromUtf8(":/data/img/software_correct.png"));
69 			m_mostlyCorrectIconPixmap = QPixmap(QString::fromUtf8(":/data/img/software_mostlycorrect.png"));
70 			m_incorrectIconPixmap = QPixmap(QString::fromUtf8(":/data/img/software_incorrect.png"));
71 			m_notFoundIconPixmap = QPixmap(QString::fromUtf8(":/data/img/software_notfound.png"));
72 			m_unknownIconPixmap = QPixmap(QString::fromUtf8(":/data/img/software_unknown.png"));
73 			hideStateFilter(); // FIXME: add software-state filtering
74 			break;
75 		case QMC2_ROMALYZER_MODE_SYSTEM:
76 		default:
77 			m_settingsKey = "CollectionRebuilder";
78 			m_defaultSetEntity = "machine";
79 			m_defaultRomEntity = "rom";
80 			m_defaultDiskEntity = "disk";
81 			checkBoxFilterExpressionSoftwareLists->setVisible(false);
82 			comboBoxFilterSyntaxSoftwareLists->setVisible(false);
83 			comboBoxFilterTypeSoftwareLists->setVisible(false);
84 			lineEditFilterExpressionSoftwareLists->setVisible(false);
85 			toolButtonClearFilterExpressionSoftwareLists->setVisible(false);
86 			toolButtonExactMatchSoftwareLists->setVisible(false);
87 			checkBoxFilterExpression->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpression", false).toBool());
88 			comboBoxFilterSyntax->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntax", 0).toInt());
89 			comboBoxFilterType->setCurrentIndex(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterType", 0).toInt());
90 			toolButtonExactMatch->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatch", false).toBool());
91 			lineEditFilterExpression->setText(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpression", QString()).toString());
92 			m_correctIconPixmap = QPixmap(QString::fromUtf8(":/data/img/sphere_green.png"));
93 			m_mostlyCorrectIconPixmap = QPixmap(QString::fromUtf8(":/data/img/sphere_yellowgreen.png"));
94 			m_incorrectIconPixmap = QPixmap(QString::fromUtf8(":/data/img/sphere_red.png"));
95 			m_notFoundIconPixmap = QPixmap(QString::fromUtf8(":/data/img/sphere_grey.png"));
96 			m_unknownIconPixmap = QPixmap(QString::fromUtf8(":/data/img/sphere_blue.png"));
97 			break;
98 	}
99 	checkBoxFilterStates->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseStateFilter", false).toBool());
100 	toolButtonStateC->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateC", true).toBool());
101 	toolButtonStateM->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateM", true).toBool());
102 	toolButtonStateI->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateI", true).toBool());
103 	toolButtonStateN->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateN", true).toBool());
104 	toolButtonStateU->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateU", true).toBool());
105 
106 	pushButtonPauseResume->setVisible(false);
107 	setIgnoreCheckpoint(false);
108 
109 	QFont logFont;
110 	logFont.fromString(qmc2Config->value(QMC2_FRONTEND_PREFIX + "GUI/LogFont").toString());
111 	plainTextEditLog->setFont(logFont);
112 	spinBoxMaxLogSize->setValue(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/MaxLogSize", 10000).toInt());
113 	checkBoxEnableLog->setChecked(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/EnableLog", true).toBool());
114 	adjustIconSizes();
115 
116 	m_rebuilderThread = new CollectionRebuilderThread(this);
117 	connect(rebuilderThread(), SIGNAL(log(const QString &)), this, SLOT(log(const QString &)));
118 	connect(rebuilderThread(), SIGNAL(rebuildStarted()), this, SLOT(rebuilderThread_rebuildStarted()));
119 	connect(rebuilderThread(), SIGNAL(rebuildFinished()), this, SLOT(rebuilderThread_rebuildFinished()));
120 	connect(rebuilderThread(), SIGNAL(rebuildPaused()), this, SLOT(rebuilderThread_rebuildPaused()));
121 	connect(rebuilderThread(), SIGNAL(rebuildResumed()), this, SLOT(rebuilderThread_rebuildResumed()));
122 	connect(rebuilderThread(), SIGNAL(progressTextChanged(const QString &)), this, SLOT(rebuilderThread_progressTextChanged(const QString &)));
123 	connect(rebuilderThread(), SIGNAL(progressRangeChanged(int, int)), this, SLOT(rebuilderThread_progressRangeChanged(int, int)));
124 	connect(rebuilderThread(), SIGNAL(progressChanged(int)), this, SLOT(rebuilderThread_progressChanged(int)));
125 	connect(rebuilderThread(), SIGNAL(statusUpdated(quint64, quint64, quint64)), this, SLOT(rebuilderThread_statusUpdated(quint64, quint64, quint64)));
126 	connect(rebuilderThread(), SIGNAL(newMissing(QString, QString, QString, QString, QString, QString, QString)), this, SLOT(rebuilderThread_newMissing(QString, QString, QString, QString, QString, QString, QString)));
127 
128 	m_missingDumpsViewer = 0;
129 	setDefaultEmulator(true);
130 
131 	m_iconCheckpoint = QIcon(QString::fromUtf8(":/data/img/checkpoint.png"));
132 	m_iconNoCheckpoint = QIcon(QString::fromUtf8(":/data/img/no_checkpoint.png"));
133 
134 	m_animationSequence = 0;
135 	connect(&m_animationTimer, SIGNAL(timeout()), this, SLOT(animationTimer_timeout()));
136 
137 	frameEntities->setVisible(false);
138 	toolButtonRemoveXmlSource->setVisible(false);
139 	comboBoxXmlSource->blockSignals(true);
140 	comboBoxXmlSource->clear();
141 	comboBoxXmlSource->insertItem(0, tr("Current default emulator"));
142 	if ( qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/Checkpoint", -1).toLongLong() >= 0 ) {
143 		comboBoxXmlSource->setItemIcon(0, m_iconCheckpoint);
144 		comboBoxXmlSource->setItemData(0, true);
145 	} else {
146 		comboBoxXmlSource->setItemIcon(0, m_iconNoCheckpoint);
147 		comboBoxXmlSource->setItemData(0, false);
148 	}
149 	QStringList xmlSources = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", QStringList()).toStringList();
150 	QStringList setEntities = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", QStringList()).toStringList();
151 	QStringList romEntities = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", QStringList()).toStringList();
152 	QStringList diskEntities = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", QStringList()).toStringList();
153 	QStringList checkpointList = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", QStringList()).toStringList();
154 	QStringList softwareCheckpointList = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", QStringList()).toStringList();
155 	bool softwareCheckpointListOk = romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? (softwareCheckpointList.count() == xmlSources.count()) : true;
156 	QList<qint64> checkpoints;
157 	foreach (QString cp, checkpointList)
158 		checkpoints << cp.toLongLong();
159 	int index = 1;
160 	if ( xmlSources.count() > 0 && setEntities.count() == xmlSources.count() && romEntities.count() == xmlSources.count() && diskEntities.count() == xmlSources.count() && checkpointList.count() == xmlSources.count() && softwareCheckpointListOk ) {
161 		for (int i = 0; i < xmlSources.count(); i++) {
162 			QString xmlSource(xmlSources.at(i));
163 			QFileInfo fi(xmlSource);
164 			if ( fi.exists() && fi.isReadable() ) {
165 				comboBoxXmlSource->insertItem(index, xmlSource);
166 				if ( checkpoints[i] >= 0 ) {
167 					comboBoxXmlSource->setItemIcon(index, m_iconCheckpoint);
168 					comboBoxXmlSource->setItemData(index, true);
169 				} else {
170 					comboBoxXmlSource->setItemIcon(index, m_iconNoCheckpoint);
171 					comboBoxXmlSource->setItemData(index, false);
172 				}
173 				index++;
174 			} else {
175 				xmlSources.removeAt(i);
176 				setEntities.removeAt(i);
177 				romEntities.removeAt(i);
178 				diskEntities.removeAt(i);
179 				checkpointList.removeAt(i);
180 				checkpoints.removeAt(i);
181 				if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
182 					softwareCheckpointList.removeAt(i);
183 				i--;
184 			}
185 		}
186 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", xmlSources);
187 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", setEntities);
188 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", romEntities);
189 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", diskEntities);
190 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", checkpointList);
191 		if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
192 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", softwareCheckpointList);
193 	} else {
194 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources");
195 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities");
196 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities");
197 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities");
198 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints");
199 		qmc2Config->remove(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints");
200 	}
201 	comboBoxXmlSource->insertSeparator(index);
202 	index++;
203 	comboBoxXmlSource->insertItem(index, tr("Select XML file..."));
204 	comboBoxXmlSource->setItemIcon(index, QIcon(QString::fromUtf8(":/data/img/fileopen.png")));
205 	comboBoxXmlSource->setCurrentIndex(0);
206 	comboBoxXmlSource->blockSignals(false);
207 	lineEditSetEntity->setText(m_defaultSetEntity);
208 	lineEditRomEntity->setText(m_defaultRomEntity);
209 	lineEditDiskEntity->setText(m_defaultDiskEntity);
210 	rebuilderThread_statusUpdated(0, 0, 0);
211 	comboBoxXmlSource->setFocus();
212 }
213 
~CollectionRebuilder()214 CollectionRebuilder::~CollectionRebuilder()
215 {
216 	hideEvent(0);
217 	if ( missingDumpsViewer() )
218 		delete missingDumpsViewer();
219 	if ( rebuilderThread() )
220 		delete rebuilderThread();
221 }
222 
on_spinBoxMaxLogSize_valueChanged(int value)223 void CollectionRebuilder::on_spinBoxMaxLogSize_valueChanged(int value)
224 {
225 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/MaxLogSize", value);
226 	plainTextEditLog->setMaximumBlockCount(value);
227 }
228 
log(const QString & message)229 void CollectionRebuilder::log(const QString &message)
230 {
231 	if ( checkBoxEnableLog->isChecked() )
232 		plainTextEditLog->appendPlainText(QDateTime::currentDateTime().toString("hh:mm:ss.zzz") + ": " + message);
233 }
234 
scrollToEnd()235 void CollectionRebuilder::scrollToEnd()
236 {
237 	plainTextEditLog->horizontalScrollBar()->setValue(plainTextEditLog->horizontalScrollBar()->minimum());
238 	plainTextEditLog->verticalScrollBar()->setValue(plainTextEditLog->verticalScrollBar()->maximum());
239 }
240 
adjustIconSizes()241 void CollectionRebuilder::adjustIconSizes()
242 {
243 	QFont f;
244 	f.fromString(qmc2Config->value(QMC2_FRONTEND_PREFIX + "GUI/Font").toString());
245 	QFontMetrics fm(f);
246 	QSize iconSize = QSize(fm.height() - 2, fm.height() - 2);
247 
248 	pushButtonStartStop->setIconSize(iconSize);
249 	pushButtonPauseResume->setIconSize(iconSize);
250 	comboBoxXmlSource->setIconSize(iconSize);
251 	toolButtonRemoveXmlSource->setIconSize(iconSize);
252 	toolButtonViewMissingList->setIconSize(iconSize);
253 	toolButtonExactMatch->setIconSize(iconSize);
254 	toolButtonClearFilterExpression->setIconSize(iconSize);
255 	toolButtonExactMatchSoftwareLists->setIconSize(iconSize);
256 	toolButtonClearFilterExpressionSoftwareLists->setIconSize(iconSize);
257 
258 	QSize iconSizeMiddle = QSize(fm.height(), fm.height());
259 	toolButtonStateC->setIcon(QIcon(m_correctIconPixmap.scaled(iconSizeMiddle, Qt::KeepAspectRatio, Qt::SmoothTransformation)));
260 	toolButtonStateM->setIcon(QIcon(m_mostlyCorrectIconPixmap.scaled(iconSizeMiddle, Qt::KeepAspectRatio, Qt::SmoothTransformation)));
261 	toolButtonStateI->setIcon(QIcon(m_incorrectIconPixmap.scaled(iconSizeMiddle, Qt::KeepAspectRatio, Qt::SmoothTransformation)));
262 	toolButtonStateN->setIcon(QIcon(m_notFoundIconPixmap.scaled(iconSizeMiddle, Qt::KeepAspectRatio, Qt::SmoothTransformation)));
263 	toolButtonStateU->setIcon(QIcon(m_unknownIconPixmap.scaled(iconSizeMiddle, Qt::KeepAspectRatio, Qt::SmoothTransformation)));
264 }
265 
on_pushButtonStartStop_clicked()266 void CollectionRebuilder::on_pushButtonStartStop_clicked()
267 {
268 	pushButtonStartStop->setEnabled(false);
269 	pushButtonStartStop->update();
270 	pushButtonPauseResume->setEnabled(false);
271 	pushButtonPauseResume->update();
272 	qApp->processEvents();
273 	if ( rebuilderThread()->isActive )
274 		rebuilderThread()->stopRebuilding = true;
275 	else if ( rebuilderThread()->isWaiting ) {
276 		newMissingList().clear();
277 		if ( comboBoxXmlSource->itemData(comboBoxXmlSource->currentIndex()).toBool() ) {
278 			if ( !ignoreCheckpoint() ) {
279 				switch ( QMessageBox::question(this, tr("Confirm checkpoint restart"), tr("Restart from stored checkpoint?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) ) {
280 					case QMessageBox::Yes: {
281 							qint64 cp = 0;
282 							int index = comboBoxXmlSource->currentIndex();
283 							if ( index == 0 )
284 								cp = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/Checkpoint", -1).toLongLong();
285 							else {
286 								index -= 1;
287 								QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", QStringList()).toStringList());
288 								QStringList softwareCheckpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", QStringList()).toStringList());
289 								if ( index >= 0 && index < checkpointList.count() ) {
290 									cp = checkpointList[index].toLongLong();
291 									if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
292 										rebuilderThread()->setListCheckpoint(softwareCheckpointList[index], index);
293 								} else {
294 									cp = -1;
295 									if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
296 										rebuilderThread()->setListCheckpoint(QString(), index);
297 								}
298 							}
299 							setDefaultEmulator(comboBoxXmlSource->currentIndex() == 0);
300 							setIgnoreCheckpoint(false);
301 							rebuilderThread()->checkpointRestart(cp);
302 						}
303 						break;
304 					case QMessageBox::No:
305 						rebuilderThread()->setCheckpoint(-1, comboBoxXmlSource->currentIndex());
306 						if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
307 							rebuilderThread()->setListCheckpoint(QString(), comboBoxXmlSource->currentIndex());
308 						setDefaultEmulator(comboBoxXmlSource->currentIndex() == 0);
309 						setIgnoreCheckpoint(false);
310 						break;
311 					case QMessageBox::Cancel:
312 					default:
313 						pushButtonStartStop->setEnabled(true);
314 						pushButtonPauseResume->setEnabled(true);
315 						return;
316 				}
317 			} else {
318 				rebuilderThread()->setCheckpoint(-1, comboBoxXmlSource->currentIndex());
319 				if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
320 					rebuilderThread()->setListCheckpoint(QString(), comboBoxXmlSource->currentIndex());
321 				setDefaultEmulator(comboBoxXmlSource->currentIndex() == 0);
322 				setIgnoreCheckpoint(false);
323 			}
324 		} else {
325 			rebuilderThread()->setCheckpoint(-1, comboBoxXmlSource->currentIndex());
326 			if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE )
327 				rebuilderThread()->setListCheckpoint(QString(), comboBoxXmlSource->currentIndex());
328 			setDefaultEmulator(comboBoxXmlSource->currentIndex() == 0);
329 			setIgnoreCheckpoint(false);
330 		}
331 		if ( missingDumpsViewer() ) {
332 			missingDumpsViewer()->setDefaultEmulator(defaultEmulator());
333 			// FIXME "non-default emulator"
334 			//missingDumpsViewer()->frameExport->setVisible(romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? false : true);
335 			missingDumpsViewer()->frameExport->setVisible(romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? false : defaultEmulator());
336 		}
337 		if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
338 			if ( checkBoxFilterExpressionSoftwareLists->isChecked() && !lineEditFilterExpressionSoftwareLists->text().isEmpty() )
339 				rebuilderThread()->setFilterExpressionSoftware(lineEditFilterExpressionSoftwareLists->text(), comboBoxFilterSyntaxSoftwareLists->currentIndex(), comboBoxFilterTypeSoftwareLists->currentIndex(), toolButtonExactMatchSoftwareLists->isChecked());
340 			else
341 				rebuilderThread()->clearFilterExpressionSoftware();
342 		}
343 		if ( checkBoxFilterExpression->isChecked() && !lineEditFilterExpression->text().isEmpty() )
344 			rebuilderThread()->setFilterExpression(lineEditFilterExpression->text(), comboBoxFilterSyntax->currentIndex(), comboBoxFilterType->currentIndex(), toolButtonExactMatch->isChecked());
345 		else
346 			rebuilderThread()->clearFilterExpression();
347 		if ( checkBoxFilterStates->isChecked() && defaultEmulator() )
348 			rebuilderThread()->setStateFilter(true, toolButtonStateC->isChecked(), toolButtonStateM->isChecked(), toolButtonStateI->isChecked(), toolButtonStateN->isChecked(), toolButtonStateU->isChecked());
349 		else
350 			rebuilderThread()->clearStateFilter();
351 		rebuilderThread()->useHashCache = romAlyzer()->checkBoxCollectionRebuilderHashCache->isChecked();
352 		rebuilderThread()->dryRun = romAlyzer()->checkBoxCollectionRebuilderDryRun->isChecked();
353 		rebuilderThread()->waitCondition.wakeAll();
354 	}
355 }
356 
on_pushButtonPauseResume_clicked()357 void CollectionRebuilder::on_pushButtonPauseResume_clicked()
358 {
359 	pushButtonPauseResume->setEnabled(false);
360 	if ( rebuilderThread()->isPaused )
361 		QTimer::singleShot(0, rebuilderThread(), SLOT(resume()));
362 	else
363 		QTimer::singleShot(0, rebuilderThread(), SLOT(pause()));
364 }
365 
setStateFilterVisibility(bool visible)366 void CollectionRebuilder::setStateFilterVisibility(bool visible)
367 {
368 	checkBoxFilterStates->setVisible(visible);
369 	toolButtonStateC->setVisible(visible);
370 	toolButtonStateM->setVisible(visible);
371 	toolButtonStateI->setVisible(visible);
372 	toolButtonStateN->setVisible(visible);
373 	toolButtonStateU->setVisible(visible);
374 }
375 
on_comboBoxXmlSource_currentIndexChanged(int index)376 void CollectionRebuilder::on_comboBoxXmlSource_currentIndexChanged(int index)
377 {
378 	static int lastIndex = -1;
379 
380 	if ( index == 0 ) {
381 		if ( lastIndex >= 0 ) {
382 			QStringList setEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", QStringList()).toStringList());
383 			QStringList romEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", QStringList()).toStringList());
384 			QStringList diskEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", QStringList()).toStringList());
385 			if ( lastIndex < setEntities.count() ) {
386 				setEntities.replace(lastIndex, lineEditSetEntity->text());
387 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", setEntities);
388 			}
389 			if ( lastIndex < romEntities.count() ) {
390 				romEntities.replace(lastIndex, lineEditRomEntity->text());
391 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", romEntities);
392 			}
393 			if ( lastIndex < diskEntities.count() ) {
394 				diskEntities.replace(lastIndex, lineEditDiskEntity->text());
395 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", diskEntities);
396 			}
397 		}
398 		lineEditSetEntity->setText(m_defaultSetEntity);
399 		lineEditRomEntity->setText(m_defaultRomEntity);
400 		lineEditDiskEntity->setText(m_defaultDiskEntity);
401 		frameEntities->setVisible(false);
402 		if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SYSTEM )
403 			showStateFilter(); // FIXME: add software-state filteirng
404 		QTimer::singleShot(0, this, SLOT(scrollToEnd()));
405 		toolButtonRemoveXmlSource->setVisible(false);
406 		lastIndex = -1;
407 	} else if ( index == comboBoxXmlSource->count() - 1 ) {
408 		QString xmlSource(QFileDialog::getOpenFileName(this, tr("Choose source XML file"), QString(), tr("Data and XML files (*.dat *.xml)") + ";;" + tr("Data files (*.dat)") + ";;" + tr("XML files (*.xml)") + ";;" + tr("All files (*)"), 0, qmc2Options->useNativeFileDialogs() ? (QFileDialog::Options)0 : QFileDialog::DontUseNativeDialog));
409 		if ( !xmlSource.isNull() ) {
410 			int foundAtIndex = comboBoxXmlSource->findText(xmlSource);
411 			if ( foundAtIndex < 0 ) {
412 				comboBoxXmlSource->blockSignals(true);
413 				QStringList xmlSources(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", QStringList()).toStringList());
414 				xmlSources << xmlSource;
415 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", xmlSources);
416 				QStringList setEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", QStringList()).toStringList());
417 				setEntities << m_defaultSetEntity;
418 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", setEntities);
419 				lineEditSetEntity->setText(m_defaultSetEntity);
420 				QStringList romEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", QStringList()).toStringList());
421 				romEntities << m_defaultRomEntity;
422 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", romEntities);
423 				lineEditRomEntity->setText(m_defaultRomEntity);
424 				QStringList diskEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", QStringList()).toStringList());
425 				diskEntities << m_defaultDiskEntity;
426 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", diskEntities);
427 				lineEditDiskEntity->setText(m_defaultDiskEntity);
428 				QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", QStringList()).toStringList());
429 				checkpointList << "-1";
430 				qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", checkpointList);
431 				if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
432 					QStringList softwareCheckpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", QStringList()).toStringList());
433 					softwareCheckpointList << QString();
434 					qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", softwareCheckpointList);
435 				}
436 				int insertIndex = comboBoxXmlSource->count() - 2;
437 				lastIndex = insertIndex - 1;
438 				comboBoxXmlSource->insertItem(insertIndex, xmlSource);
439 				comboBoxXmlSource->setItemIcon(insertIndex, m_iconNoCheckpoint);
440 				comboBoxXmlSource->setItemData(insertIndex, false);
441 				comboBoxXmlSource->setCurrentIndex(insertIndex);
442 				comboBoxXmlSource->blockSignals(false);
443 				frameEntities->setVisible(true);
444 				hideStateFilter();
445 				toolButtonRemoveXmlSource->setVisible(true);
446 				QTimer::singleShot(0, this, SLOT(scrollToEnd()));
447 			} else
448 				comboBoxXmlSource->setCurrentIndex(foundAtIndex);
449 		} else
450 			comboBoxXmlSource->setCurrentIndex(0);
451 		raise();
452 	} else {
453 		index -= 1;
454 		if ( index >= 0 ) {
455 			QStringList setEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", QStringList()).toStringList());
456 			QStringList romEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", QStringList()).toStringList());
457 			QStringList diskEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", QStringList()).toStringList());
458 			if ( lastIndex >= 0 ) {
459 				if ( lastIndex < setEntities.count() ) {
460 					setEntities.replace(lastIndex, lineEditSetEntity->text());
461 					qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", setEntities);
462 				}
463 				if ( lastIndex < romEntities.count() ) {
464 					romEntities.replace(lastIndex, lineEditRomEntity->text());
465 					qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", romEntities);
466 				}
467 				if ( lastIndex < diskEntities.count() ) {
468 					diskEntities.replace(lastIndex, lineEditDiskEntity->text());
469 					qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", diskEntities);
470 				}
471 			}
472 			lastIndex = index;
473 			lineEditSetEntity->setText(setEntities[index]);
474 			lineEditRomEntity->setText(romEntities[index]);
475 			lineEditDiskEntity->setText(diskEntities[index]);
476 			frameEntities->setVisible(true);
477 			hideStateFilter();
478 			toolButtonRemoveXmlSource->setVisible(true);
479 			QTimer::singleShot(0, this, SLOT(scrollToEnd()));
480 		}
481 	}
482 }
483 
on_toolButtonRemoveXmlSource_clicked()484 void CollectionRebuilder::on_toolButtonRemoveXmlSource_clicked()
485 {
486 	int index = comboBoxXmlSource->currentIndex() - 1;
487 	comboBoxXmlSource->setCurrentIndex(0);
488 	QStringList xmlSources(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", QStringList()).toStringList());
489 	xmlSources.removeAt(index);
490 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSources", xmlSources);
491 	QStringList setEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", QStringList()).toStringList());
492 	setEntities.removeAt(index);
493 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/SetEntities", setEntities);
494 	QStringList romEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", QStringList()).toStringList());
495 	romEntities.removeAt(index);
496 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/RomEntities", romEntities);
497 	QStringList diskEntities(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", QStringList()).toStringList());
498 	diskEntities.removeAt(index);
499 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/DiskEntities", diskEntities);
500 	QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", QStringList()).toStringList());
501 	checkpointList.removeAt(index);
502 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", checkpointList);
503 	if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
504 		QStringList softwareCheckpointList = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", QStringList()).toStringList();
505 		softwareCheckpointList.removeAt(index);
506 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", softwareCheckpointList);
507 	}
508 	comboBoxXmlSource->blockSignals(true);
509 	comboBoxXmlSource->removeItem(index + 1);
510 	comboBoxXmlSource->blockSignals(false);
511 }
512 
on_toolButtonViewMissingList_clicked()513 void CollectionRebuilder::on_toolButtonViewMissingList_clicked()
514 {
515 	if ( missingDumpsViewer() ) {
516 		if ( missingDumpsViewer()->isVisible() )
517 			missingDumpsViewer()->hide();
518 		else
519 			missingDumpsViewer()->show();
520 	} else {
521 		m_missingDumpsViewer = new MissingDumpsViewer(romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? "SoftwareMissingDumpsViewer" : "MissingDumpsViewer", 0);
522 		// FIXME "non-default emulator"
523 		//missingDumpsViewer()->frameExport->setVisible(romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? false : true);
524 		missingDumpsViewer()->setDefaultEmulator(defaultEmulator());
525 		missingDumpsViewer()->frameExport->setVisible(romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? false : defaultEmulator());
526 		if ( rebuilderThread()->isActive )
527 			missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(false);
528 		else
529 			missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(!newMissingList().isEmpty() || missingDumpsViewer()->treeWidget->topLevelItemCount() > 0);
530 		if ( !newMissingList().isEmpty() )
531 			QTimer::singleShot(0, this, SLOT(updateMissingList()));
532 		missingDumpsViewer()->show();
533 	}
534 }
535 
rebuilderThread_rebuildStarted()536 void CollectionRebuilder::rebuilderThread_rebuildStarted()
537 {
538 	if ( missingDumpsViewer() ) {
539 		missingDumpsViewer()->treeWidget->clear();
540 		missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(false);
541 	}
542 	pushButtonStartStop->setIcon(QIcon(QString::fromUtf8(":/data/img/halt.png")));
543 	if ( rebuilderThread()->dryRun ) {
544 		pushButtonStartStop->setText(tr("Stop dry run"));
545 		pushButtonStartStop->setToolTip(tr("Start / stop dry run"));
546 	} else {
547 		pushButtonStartStop->setText(tr("Stop rebuilding"));
548 		pushButtonStartStop->setToolTip(tr("Start / stop rebuilding"));
549 	}
550 	pushButtonPauseResume->setText(tr("Pause"));
551 	pushButtonPauseResume->show();
552 	pushButtonStartStop->setEnabled(true);
553 	pushButtonPauseResume->setEnabled(true);
554 	comboBoxXmlSource->setEnabled(false);
555 	comboBoxXmlSource->setItemIcon(comboBoxXmlSource->currentIndex(), m_iconCheckpoint);
556 	comboBoxXmlSource->setItemData(comboBoxXmlSource->currentIndex(), true);
557 	labelXmlSource->setEnabled(false);
558 	toolButtonRemoveXmlSource->setEnabled(false);
559 	comboBoxModeSwitch->setEnabled(false);
560 	checkBoxFilterExpression->setEnabled(false);
561 	checkBoxFilterExpressionSoftwareLists->setEnabled(false);
562 	comboBoxFilterSyntax->setEnabled(false);
563 	comboBoxFilterSyntaxSoftwareLists->setEnabled(false);
564 	comboBoxFilterType->setEnabled(false);
565 	comboBoxFilterTypeSoftwareLists->setEnabled(false);
566 	lineEditFilterExpression->setEnabled(false);
567 	lineEditFilterExpressionSoftwareLists->setEnabled(false);
568 	toolButtonExactMatch->setEnabled(false);
569 	toolButtonExactMatchSoftwareLists->setEnabled(false);
570 	toolButtonClearFilterExpression->setEnabled(false);
571 	toolButtonClearFilterExpressionSoftwareLists->setEnabled(false);
572 	checkBoxFilterStates->setEnabled(false);
573 	toolButtonStateC->setEnabled(false);
574 	toolButtonStateM->setEnabled(false);
575 	toolButtonStateI->setEnabled(false);
576 	toolButtonStateN->setEnabled(false);
577 	toolButtonStateU->setEnabled(false);
578 	frameEntities->setEnabled(false);
579 	romAlyzer()->groupBoxCheckSumDatabase->setEnabled(false);
580 	romAlyzer()->groupBoxSetRewriter->setEnabled(false);
581 	romAlyzer()->checkBoxCollectionRebuilderDryRun->setEnabled(false);
582 	m_animationSequence = 0;
583 	m_animationTimer.start(QMC2_ROMALYZER_REBUILD_ANIM_SPEED);
584 	if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE && qmc2SoftwareList ) {
585 		qmc2SoftwareList->rebuildMenuAction->setEnabled(false);
586 		qmc2SoftwareList->actionRebuildSoftware->setEnabled(false);
587 		qmc2SoftwareList->actionRebuildSoftwareList->setEnabled(false);
588 		qmc2SoftwareList->actionRebuildSoftwareLists->setEnabled(false);
589 		qmc2SoftwareList->toolButtonRebuildSoftware->setEnabled(false);
590 	}
591 	qApp->processEvents();
592 }
593 
rebuilderThread_rebuildFinished()594 void CollectionRebuilder::rebuilderThread_rebuildFinished()
595 {
596 	int index = comboBoxXmlSource->currentIndex();
597 	qint64 cp = rebuilderThread()->checkpoint();
598 	if ( index == 0 )
599 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/Checkpoint", cp);
600 	else {
601 		int xmlSourceIndex = index - 1;
602 		QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", QStringList()).toStringList());
603 		if ( xmlSourceIndex >= 0 && xmlSourceIndex < checkpointList.count() ) {
604 			checkpointList.replace(xmlSourceIndex, QString::number(cp));
605 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceCheckpoints", checkpointList);
606 			if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
607 				QStringList softwareCheckpointList = qmc2Config->value(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", QStringList()).toStringList();
608 				if ( xmlSourceIndex < softwareCheckpointList.count() ) {
609 					softwareCheckpointList.replace(xmlSourceIndex, rebuilderThread()->currentListName());
610 					qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/XmlSourceListCheckpoints", softwareCheckpointList);
611 				}
612 			}
613 		}
614 	}
615 	if ( index < 0 )
616 		return;
617 	if ( cp >= 0 ) {
618 		comboBoxXmlSource->setItemIcon(index, m_iconCheckpoint);
619 		comboBoxXmlSource->setItemData(index, true);
620 	} else {
621 		comboBoxXmlSource->setItemIcon(index, m_iconNoCheckpoint);
622 		comboBoxXmlSource->setItemData(index, false);
623 	}
624 	if ( missingDumpsViewer() ) {
625 		missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(!newMissingList().isEmpty() || missingDumpsViewer()->treeWidget->topLevelItemCount() > 0);
626 		missingDumpsViewer()->setDefaultEmulator(defaultEmulator());
627 	}
628 	pushButtonStartStop->setIcon(QIcon(QString::fromUtf8(":/data/img/refresh.png")));
629 	if ( romAlyzer()->checkBoxCollectionRebuilderDryRun->isChecked() ) {
630 		pushButtonStartStop->setText(tr("Start dry run"));
631 		pushButtonStartStop->setToolTip(tr("Start / stop dry run"));
632 	} else {
633 		pushButtonStartStop->setText(tr("Start rebuilding"));
634 		pushButtonStartStop->setToolTip(tr("Start / stop rebuilding"));
635 	}
636 	pushButtonPauseResume->hide();
637 	pushButtonStartStop->setEnabled(true);
638 	pushButtonPauseResume->setEnabled(true);
639 	comboBoxXmlSource->setEnabled(true);
640 	labelXmlSource->setEnabled(true);
641 	toolButtonRemoveXmlSource->setEnabled(true);
642 	comboBoxModeSwitch->setEnabled(true);
643 	checkBoxFilterExpression->setEnabled(true);
644 	checkBoxFilterExpressionSoftwareLists->setEnabled(true);
645 	comboBoxFilterSyntax->setEnabled(checkBoxFilterExpression->isChecked());
646 	comboBoxFilterSyntaxSoftwareLists->setEnabled(checkBoxFilterExpressionSoftwareLists->isChecked());
647 	comboBoxFilterType->setEnabled(checkBoxFilterExpression->isChecked());
648 	comboBoxFilterTypeSoftwareLists->setEnabled(checkBoxFilterExpressionSoftwareLists->isChecked());
649 	lineEditFilterExpression->setEnabled(checkBoxFilterExpression->isChecked());
650 	lineEditFilterExpressionSoftwareLists->setEnabled(checkBoxFilterExpressionSoftwareLists->isChecked());
651 	toolButtonExactMatch->setEnabled(checkBoxFilterExpression->isChecked());
652 	toolButtonExactMatchSoftwareLists->setEnabled(checkBoxFilterExpressionSoftwareLists->isChecked());
653 	toolButtonClearFilterExpression->setEnabled(checkBoxFilterExpression->isChecked());
654 	toolButtonClearFilterExpressionSoftwareLists->setEnabled(checkBoxFilterExpressionSoftwareLists->isChecked());
655 	checkBoxFilterStates->setEnabled(true);
656 	toolButtonStateC->setEnabled(checkBoxFilterStates->isChecked());
657 	toolButtonStateM->setEnabled(checkBoxFilterStates->isChecked());
658 	toolButtonStateI->setEnabled(checkBoxFilterStates->isChecked());
659 	toolButtonStateN->setEnabled(checkBoxFilterStates->isChecked());
660 	toolButtonStateU->setEnabled(checkBoxFilterStates->isChecked());
661 	frameEntities->setEnabled(true);
662 	romAlyzer()->groupBoxCheckSumDatabase->setEnabled(true);
663 	romAlyzer()->groupBoxSetRewriter->setEnabled(true);
664 	romAlyzer()->checkBoxCollectionRebuilderDryRun->setEnabled(true);
665 	m_animationTimer.stop();
666 	romAlyzer()->tabWidgetAnalysis->setTabIcon(QMC2_ROMALYZER_PAGE_RCR, QIcon(QString::fromUtf8(":/data/img/rebuild.png")));
667 	if ( romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE && qmc2SoftwareList ) {
668 		qmc2SoftwareList->rebuildMenuAction->setEnabled(true);
669 		qmc2SoftwareList->actionRebuildSoftware->setEnabled(true);
670 		qmc2SoftwareList->actionRebuildSoftwareList->setEnabled(true);
671 		qmc2SoftwareList->actionRebuildSoftwareLists->setEnabled(true);
672 		qmc2SoftwareList->toolButtonRebuildSoftware->setEnabled(true);
673 	}
674 	qApp->processEvents();
675 	if ( !newMissingList().isEmpty() && missingDumpsViewer() )
676 		QTimer::singleShot(0, this, SLOT(updateMissingList()));
677 }
678 
rebuilderThread_rebuildPaused()679 void CollectionRebuilder::rebuilderThread_rebuildPaused()
680 {
681 	pushButtonPauseResume->setText(tr("Resume"));
682 	pushButtonPauseResume->setEnabled(true);
683 }
684 
rebuilderThread_rebuildResumed()685 void CollectionRebuilder::rebuilderThread_rebuildResumed()
686 {
687 	pushButtonPauseResume->setText(tr("Pause"));
688 	pushButtonPauseResume->setEnabled(true);
689 }
690 
rebuilderThread_progressTextChanged(const QString & text)691 void CollectionRebuilder::rebuilderThread_progressTextChanged(const QString &text)
692 {
693 	progressBar->setFormat(text);
694 }
695 
rebuilderThread_progressRangeChanged(int min,int max)696 void CollectionRebuilder::rebuilderThread_progressRangeChanged(int min, int max)
697 {
698 	progressBar->setRange(min, max);
699 }
700 
rebuilderThread_progressChanged(int progress)701 void CollectionRebuilder::rebuilderThread_progressChanged(int progress)
702 {
703 	progressBar->setValue(progress);
704 }
705 
rebuilderThread_statusUpdated(quint64 setsProcessed,quint64 missingDumps,quint64 missingDisks)706 void CollectionRebuilder::rebuilderThread_statusUpdated(quint64 setsProcessed, quint64 missingDumps, quint64 missingDisks)
707 {
708 	QString statusString("<table border=\"0\" cellpadding=\"0\" cellspacing=\"4\" width=\"100%\"><tr>");
709 	statusString += "<td nowrap align=\"left\" width=\"16.16%\"><b>" + tr("Sets processed") + ":</b></td><td nowrap align=\"right\" width=\"16.16%\">" + QString::number(setsProcessed) + "</td>";
710 	statusString += "<td nowrap align=\"center\" width=\"1%\">|</td>";
711 	statusString += "<td nowrap align=\"left\" width=\"16.16%\"><b>" + tr("Missing ROMs") + ":</b></td><td nowrap align=\"right\" width=\"16.16%\">" + QString::number(missingDumps) + "</td>";
712 	statusString += "<td nowrap align=\"center\" width=\"1%\">|</td>";
713 	statusString += "<td nowrap align=\"left\" width=\"16.16%\"><b>" + tr("Missing disks") + ":</b></td><td nowrap align=\"right\" width=\"16.16%\">" + QString::number(missingDisks) + "</td>";
714 	statusString += "<td nowrap align=\"right\" width=\"1%\">|</td>";
715 	statusString += "</tr></table>";
716 	labelRebuildStatus->setText(statusString);
717 }
718 
rebuilderThread_newMissing(QString id,QString type,QString name,QString size,QString crc,QString sha1,QString reason)719 void CollectionRebuilder::rebuilderThread_newMissing(QString id, QString type, QString name, QString size, QString crc, QString sha1, QString reason)
720 {
721 	newMissingList() << id + "|" + type + "|" + name + "|" + size + "|" + crc + "|" + sha1 + "|" + reason;
722 }
723 
animationTimer_timeout()724 void CollectionRebuilder::animationTimer_timeout()
725 {
726 	m_animationTimer.stop();
727 	QPixmap pixmap(QString::fromUtf8(":/data/img/rebuild.png"));
728 	QPixmap rotatedPixmap(pixmap.size());
729 	rotatedPixmap.fill(Qt::transparent);
730 	QPainter p(&rotatedPixmap);
731 	QSize size = pixmap.size();
732 	p.translate(size.height()/2,size.height()/2);
733 	p.rotate(24 * ++m_animationSequence);
734 	if ( m_animationSequence > 14 )
735 		m_animationSequence = 0;
736 	p.translate(-size.height()/2,-size.height()/2);
737 	p.drawPixmap(0, 0, pixmap);
738 	p.end();
739 	romAlyzer()->tabWidgetAnalysis->setTabIcon(QMC2_ROMALYZER_PAGE_RCR, QIcon(rotatedPixmap));
740 	m_animationTimer.start(QMC2_ROMALYZER_REBUILD_ANIM_SPEED);
741 	if ( !newMissingList().isEmpty() && missingDumpsViewer() )
742 		QTimer::singleShot(0, this, SLOT(updateMissingList()));
743 }
744 
updateMissingList()745 void CollectionRebuilder::updateMissingList()
746 {
747 	QList<QTreeWidgetItem *> itemList;
748 	foreach (QString newMissing, newMissingList()) {
749 		QStringList missingWords(newMissing.split('|'));
750 		if ( missingWords.count() >= 7 ) {
751 			QTreeWidgetItem *newItem = new QTreeWidgetItem(0);
752 			newItem->setText(QMC2_MDV_COLUMN_ID, missingWords.at(0));
753 			newItem->setText(QMC2_MDV_COLUMN_TYPE, missingWords.at(1));
754 			newItem->setText(QMC2_MDV_COLUMN_NAME, missingWords.at(2));
755 			newItem->setText(QMC2_MDV_COLUMN_SIZE, missingWords.at(3));
756 			newItem->setText(QMC2_MDV_COLUMN_CRC, missingWords.at(4));
757 			newItem->setText(QMC2_MDV_COLUMN_SHA1, missingWords.at(5));
758 			newItem->setText(QMC2_MDV_COLUMN_REASON, missingWords.at(6));
759 			itemList << newItem;
760 		}
761 	}
762 	missingDumpsViewer()->treeWidget->insertTopLevelItems(0, itemList);
763 	newMissingList().clear();
764 	if ( rebuilderThread()->isActive )
765 		missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(false);
766 	else
767 		missingDumpsViewer()->toolButtonExportToDataFile->setEnabled(missingDumpsViewer()->treeWidget->topLevelItemCount() > 0);
768 }
769 
updateModeSetup()770 void CollectionRebuilder::updateModeSetup()
771 {
772 	if ( active() ) {
773 		if ( rebuilderThread()->dryRun ) {
774 			pushButtonStartStop->setText(tr("Stop dry run"));
775 			pushButtonStartStop->setToolTip(tr("Start / stop dry run"));
776 		} else {
777 			pushButtonStartStop->setText(tr("Stop rebuilding"));
778 			pushButtonStartStop->setToolTip(tr("Start / stop rebuilding"));
779 		}
780 	} else {
781 		if ( romAlyzer()->checkBoxCollectionRebuilderDryRun->isChecked() ){
782 			pushButtonStartStop->setText(tr("Start dry run"));
783 			pushButtonStartStop->setToolTip(tr("Start / stop dry run"));
784 			comboBoxModeSwitch->blockSignals(true);
785 			comboBoxModeSwitch->setCurrentIndex(1);
786 			comboBoxModeSwitch->blockSignals(false);
787 		} else {
788 			pushButtonStartStop->setText(tr("Start rebuilding"));
789 			pushButtonStartStop->setToolTip(tr("Start / stop rebuilding"));
790 			comboBoxModeSwitch->blockSignals(true);
791 			comboBoxModeSwitch->setCurrentIndex(0);
792 			comboBoxModeSwitch->blockSignals(false);
793 		}
794 	}
795 }
796 
on_comboBoxModeSwitch_currentIndexChanged(int index)797 void CollectionRebuilder::on_comboBoxModeSwitch_currentIndexChanged(int index)
798 {
799 	if ( index == 0 ) {
800 		romAlyzer()->checkBoxCollectionRebuilderDryRun->setChecked(false);
801 		pushButtonStartStop->setText(tr("Start rebuilding"));
802 		pushButtonStartStop->setToolTip(tr("Start / stop rebuilding"));
803 	} else {
804 		romAlyzer()->checkBoxCollectionRebuilderDryRun->setChecked(true);
805 		pushButtonStartStop->setText(tr("Start dry run"));
806 		pushButtonStartStop->setToolTip(tr("Start / stop dry run"));
807 	}
808 }
809 
showEvent(QShowEvent * e)810 void CollectionRebuilder::showEvent(QShowEvent *e)
811 {
812 	QTimer::singleShot(0, this, SLOT(updateModeSetup()));
813 }
814 
hideEvent(QHideEvent * e)815 void CollectionRebuilder::hideEvent(QHideEvent *e)
816 {
817 	switch ( romAlyzer()->mode() ) {
818 		case QMC2_ROMALYZER_MODE_SOFTWARE:
819 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpressionSoftwareLists", checkBoxFilterExpressionSoftwareLists->isChecked());
820 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntaxSoftwareLists", comboBoxFilterSyntaxSoftwareLists->currentIndex());
821 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterTypeSoftwareLists", comboBoxFilterTypeSoftwareLists->currentIndex());
822 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatchSoftwareLists", toolButtonExactMatchSoftwareLists->isChecked());
823 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpressionSoftwareLists", lineEditFilterExpressionSoftwareLists->text());
824 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpression", checkBoxFilterExpression->isChecked());
825 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntax", comboBoxFilterSyntax->currentIndex());
826 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterType", comboBoxFilterType->currentIndex());
827 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatch", toolButtonExactMatch->isChecked());
828 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpression", lineEditFilterExpression->text());
829 			break;
830 		case QMC2_ROMALYZER_MODE_SYSTEM:
831 		default:
832 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseFilterExpression", checkBoxFilterExpression->isChecked());
833 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterSyntax", comboBoxFilterSyntax->currentIndex());
834 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterType", comboBoxFilterType->currentIndex());
835 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/ExactMatch", toolButtonExactMatch->isChecked());
836 			qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/FilterExpression", lineEditFilterExpression->text());
837 			break;
838 	}
839 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/UseStateFilter", checkBoxFilterStates->isChecked());
840 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateC", toolButtonStateC->isChecked());
841 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateM", toolButtonStateM->isChecked());
842 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateI", toolButtonStateI->isChecked());
843 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateN", toolButtonStateN->isChecked());
844 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/IncludeStateU", toolButtonStateU->isChecked());
845 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + m_settingsKey + "/EnableLog", checkBoxEnableLog->isChecked());
846 	if ( missingDumpsViewer() )
847 		missingDumpsViewer()->close();
848 	if ( e )
849 		e->ignore();
850 }
851 
CollectionRebuilderThread(QObject * parent)852 CollectionRebuilderThread::CollectionRebuilderThread(QObject *parent)
853 	: QThread(parent)
854 {
855 	isActive = exitThread = isWaiting = isPaused = pauseRequested = stopRebuilding = doFilter = doFilterSoftware = isIncludeFilter = isIncludeFilterSoftware = doFilterState = false;
856 	includeStateC = includeStateM = includeStateI = includeStateN = includeStateU = true;
857 	exactMatch = exactMatchSoftware = useHashCache = dryRun = false;
858 	m_rebuilderDialog = (CollectionRebuilder *)parent;
859 	m_checkSumDb = 0;
860 	m_xmlIndex = m_xmlIndexCount = m_checkpoint = -1;
861 	m_hashCacheUpdateTime = 0;
862 	setListEntityStartPattern("<softwarelist name=\"");
863 	if ( m_replacementHash.isEmpty() ) {
864 		m_replacementHash.insert("&amp;", "&");
865 		m_replacementHash.insert("&lt;", "<");
866 		m_replacementHash.insert("&gt;", ">");
867 		m_replacementHash.insert("&quot;", "\"");
868 		m_replacementHash.insert("&apos;", "'");
869 	}
870 	if ( m_fileTypes.isEmpty() )
871 		m_fileTypes << "ZIP" << "7Z" << "CHD" << "FILE";
872 	reopenCheckSumDb();
873 	switch ( rebuilderDialog()->romAlyzer()->mode() ) {
874 		case QMC2_ROMALYZER_MODE_SOFTWARE:
875 			m_swlDb = new SoftwareListXmlDatabaseManager(this);
876 			m_xmlDb = 0;
877 			break;
878 		case QMC2_ROMALYZER_MODE_SYSTEM:
879 		default:
880 			m_xmlDb = new XmlDatabaseManager(this);
881 			m_swlDb = 0;
882 			break;
883 	}
884 	start();
885 }
886 
~CollectionRebuilderThread()887 CollectionRebuilderThread::~CollectionRebuilderThread()
888 {
889 	exitThread = true;
890 	waitCondition.wakeAll();
891 	wait();
892 	if ( checkSumDb() ) {
893 		checkSumDb()->disconnect(rebuilderDialog());
894 		delete checkSumDb();
895 	}
896 	if ( xmlDb() )
897 		delete xmlDb();
898 	if ( swlDb() )
899 		delete swlDb();
900 }
901 
reopenCheckSumDb()902 void CollectionRebuilderThread::reopenCheckSumDb()
903 {
904 	if ( checkSumDb() ) {
905 		checkSumDb()->disconnect(rebuilderDialog());
906 		delete checkSumDb();
907 	}
908 	m_checkSumDb = new CheckSumDatabaseManager(this, rebuilderDialog()->romAlyzer()->settingsKey());
909 	connect(checkSumDb(), SIGNAL(log(const QString &)), rebuilderDialog(), SLOT(log(const QString &)));
910 }
911 
parseXml(QString xml,QString * id,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList * diskNameList,QStringList * diskSha1List,QStringList * diskSizeList)912 bool CollectionRebuilderThread::parseXml(QString xml, QString *id, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList *diskNameList, QStringList *diskSha1List, QStringList *diskSizeList)
913 {
914 	static const QString statusString("status=\"");
915 	static const QString mergeString("merge=\"");
916 	static const QString sha1String("sha1=\"");
917 	static const QString crcString("crc=\"");
918 	static const QString sizeString("size=\"");
919 	static const QChar quoteChar('\"');
920 
921 	if ( xml.isEmpty() )
922 		return false;
923 
924 	int startIndex = -1;
925 	int endIndex = -1;
926 	QStringList xmlLines(xml.split('\n'));
927 	QString xmlLine(xmlLines.at(0));
928 	startIndex = xmlLine.indexOf(setEntityPattern());
929 	if ( startIndex >= 0 ) {
930 		startIndex += setEntityPattern().length();
931 		endIndex = xmlLine.indexOf(quoteChar, startIndex);
932 		if ( endIndex >= 0 ) {
933 			*id = xmlLine.mid(startIndex, endIndex - startIndex);
934 			QString mergeName, romName, status, romSha1, romCrc, romSize, diskName, diskSha1, diskSize;
935 			for (int i = 1; i < xmlLines.count(); i++) {
936 				xmlLine = xmlLines.at(i);
937 				bool romFound = false;
938 				startIndex = xmlLine.indexOf(romEntityPattern());
939 				if ( startIndex >= 0 ) {
940 					startIndex += romEntityPattern().length();
941 					endIndex = xmlLine.indexOf(quoteChar, startIndex);
942 					if ( endIndex >= 0 ) {
943 						romFound = true;
944 						romName = xmlLine.mid(startIndex, endIndex - startIndex);
945 						mergeName.clear();
946 						status.clear();
947 						startIndex = xmlLine.indexOf(statusString);
948 						if ( startIndex >= 0 ) {
949 							startIndex += statusString.length();
950 							endIndex = xmlLine.indexOf(quoteChar, startIndex);
951 							if ( endIndex >= 0 )
952 								status = xmlLine.mid(startIndex, endIndex - startIndex);
953 						}
954 						startIndex = xmlLine.indexOf(mergeString);
955 						if ( startIndex >= 0 ) {
956 							startIndex += mergeString.length();
957 							endIndex = xmlLine.indexOf(quoteChar, startIndex);
958 							if ( endIndex >= 0 )
959 								mergeName = xmlLine.mid(startIndex, endIndex - startIndex);
960 						}
961 						if ( status != "nodump" && (!merge() || mergeName.isEmpty()) ) {
962 							romSha1.clear(); romCrc.clear(); romSize.clear();
963 							startIndex = xmlLine.indexOf(sha1String);
964 							if ( startIndex >= 0 ) {
965 								startIndex += sha1String.length();
966 								endIndex = xmlLine.indexOf(quoteChar, startIndex);
967 								if ( endIndex >= 0 )
968 									romSha1 = xmlLine.mid(startIndex, endIndex - startIndex);
969 							}
970 							startIndex = xmlLine.indexOf(crcString);
971 							if ( startIndex >= 0 ) {
972 								startIndex += crcString.length();
973 								endIndex = xmlLine.indexOf(quoteChar, startIndex);
974 								if ( endIndex >= 0 )
975 									romCrc = xmlLine.mid(startIndex, endIndex - startIndex);
976 							}
977 							startIndex = xmlLine.indexOf(sizeString);
978 							if ( startIndex >= 0 ) {
979 								startIndex += sizeString.length();
980 								endIndex = xmlLine.indexOf(quoteChar, startIndex);
981 								if ( endIndex >= 0 )
982 									romSize = xmlLine.mid(startIndex, endIndex - startIndex);
983 							}
984 							if ( !romSha1.isEmpty() || !romCrc.isEmpty() ) {
985 								romNameList->append(toHumanReadable(romName));
986 								romSha1List->append(romSha1);
987 								romCrcList->append(romCrc);
988 								romSizeList->append(romSize);
989 							}
990 						}
991 					}
992 				}
993 				if ( romFound )
994 					continue;
995 				startIndex = xmlLine.indexOf(diskEntityPattern());
996 				if ( startIndex >= 0 ) {
997 					startIndex += diskEntityPattern().length();
998 					endIndex = xmlLine.indexOf(quoteChar, startIndex);
999 					if ( endIndex >= 0 ) {
1000 						diskName = xmlLine.mid(startIndex, endIndex - startIndex);
1001 						mergeName.clear();
1002 						startIndex = xmlLine.indexOf(mergeString);
1003 						if ( startIndex >= 0 ) {
1004 							startIndex += mergeString.length();
1005 							endIndex = xmlLine.indexOf(quoteChar, startIndex);
1006 							if ( endIndex >= 0 )
1007 								mergeName = xmlLine.mid(startIndex, endIndex - startIndex);
1008 						}
1009 						if ( !merge() || mergeName.isEmpty() ) {
1010 							diskSha1.clear();
1011 							startIndex = xmlLine.indexOf(sha1String);
1012 							if ( startIndex >= 0 ) {
1013 								startIndex += sha1String.length();
1014 								endIndex = xmlLine.indexOf(quoteChar, startIndex);
1015 								if ( endIndex >= 0 )
1016 									diskSha1 = xmlLine.mid(startIndex, endIndex - startIndex);
1017 							}
1018 							diskSize.clear();
1019 							startIndex = xmlLine.indexOf(sizeString);
1020 							if ( startIndex >= 0 ) {
1021 								startIndex += sizeString.length();
1022 								endIndex = xmlLine.indexOf(quoteChar, startIndex);
1023 								if ( endIndex >= 0 )
1024 									diskSize = xmlLine.mid(startIndex, endIndex - startIndex);
1025 							}
1026 							if ( !diskSha1.isEmpty() ) {
1027 								diskNameList->append(toHumanReadable(diskName));
1028 								diskSha1List->append(diskSha1);
1029 								diskSizeList->append(diskSize);
1030 							}
1031 						}
1032 					}
1033 				}
1034 			}
1035 			return true;
1036 		} else
1037 			return false;
1038 	} else
1039 		return false;
1040 }
1041 
nextId(QString * id,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList * diskNameList,QStringList * diskSha1List,QStringList * diskSizeList)1042 bool CollectionRebuilderThread::nextId(QString *id, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList *diskNameList, QStringList *diskSha1List, QStringList *diskSizeList)
1043 {
1044 	id->clear();
1045 	romNameList->clear();
1046 	romSha1List->clear();
1047 	romCrcList->clear();
1048 	romSizeList->clear();
1049 	diskNameList->clear();
1050 	diskSha1List->clear();
1051 	diskSizeList->clear();
1052 	if ( m_xmlIndex < 0 || m_xmlIndexCount < 0 ) {
1053 		if ( rebuilderDialog()->defaultEmulator() ) {
1054 			m_xmlIndex = 0;
1055 			switch ( rebuilderDialog()->romAlyzer()->mode() ) {
1056 				case QMC2_ROMALYZER_MODE_SOFTWARE:
1057 					swlDb()->initIdAtIndexCache();
1058 					m_xmlIndexCount = swlDb()->idAtIndexCacheSize();
1059 					break;
1060 				case QMC2_ROMALYZER_MODE_SYSTEM:
1061 				default:
1062 					xmlDb()->initIdAtIndexCache();
1063 					m_xmlIndexCount = xmlDb()->idAtIndexCacheSize();
1064 					break;
1065 			}
1066 			emit progressRangeChanged(m_xmlIndex, m_xmlIndexCount);
1067 			emit progressChanged(m_xmlIndex);
1068 			setCheckpoint(m_xmlIndex, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1069 			return true;
1070 		} else {
1071 			m_xmlFile.setFileName(rebuilderDialog()->comboBoxXmlSource->currentText());
1072 			if ( m_xmlFile.open(QIODevice::ReadOnly | QIODevice::Text) ) {
1073 				m_xmlIndex = 0;
1074 				m_xmlIndexCount = m_xmlFile.size() - 1;
1075 				emit progressRangeChanged(m_xmlIndex, m_xmlIndexCount);
1076 				emit progressChanged(m_xmlIndex);
1077 				setCheckpoint(0, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1078 				return true;
1079 			} else {
1080 				emit log(tr("FATAL: can't open XML file '%1' for reading, please check permissions").arg(rebuilderDialog()->comboBoxXmlSource->currentText()));
1081 				setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1082 				return false;
1083 			}
1084 		}
1085 	} else {
1086 		if ( m_xmlIndex > m_xmlIndexCount ) {
1087 			emit progressChanged(m_xmlIndexCount);
1088 			setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1089 			return false;
1090 		}
1091 		if ( rebuilderDialog()->defaultEmulator() ) {
1092 			QString setKey;
1093 			switch ( rebuilderDialog()->romAlyzer()->mode() ) {
1094 				case QMC2_ROMALYZER_MODE_SOFTWARE:
1095 					setKey = swlDb()->idAtIndex(m_xmlIndex);
1096 					if ( !evaluateFilters(setKey) ) {
1097 						m_xmlIndex++;
1098 						emit progressChanged(m_xmlIndex);
1099 						return true;
1100 					}
1101 					if ( parseXml(swlDb()->xml(setKey), id, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List, diskSizeList) ) {
1102 						*id = setKey;
1103 						m_xmlIndex++;
1104 						emit progressChanged(m_xmlIndex);
1105 						return true;
1106 					} else {
1107 						emit log(tr("FATAL: XML parsing failed"));
1108 						setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1109 						return false;
1110 					}
1111 				case QMC2_ROMALYZER_MODE_SYSTEM:
1112 				default:
1113 					setKey = xmlDb()->idAtIndex(m_xmlIndex);
1114 					if ( !evaluateFilters(setKey) ) {
1115 						m_xmlIndex++;
1116 						emit progressChanged(m_xmlIndex);
1117 						return true;
1118 					}
1119 					if ( parseXml(xmlDb()->xml(setKey), id, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List, diskSizeList) ) {
1120 						m_xmlIndex++;
1121 						emit progressChanged(m_xmlIndex);
1122 						return true;
1123 					} else {
1124 						emit log(tr("FATAL: XML parsing failed"));
1125 						setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1126 						return false;
1127 					}
1128 			}
1129 		} else {
1130 			QByteArray line(m_xmlFile.readLine());
1131 			while ( !m_xmlFile.atEnd() && line.indexOf(setEntityStartPattern()) < 0 && (rebuilderDialog()->romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ? line.indexOf(listEntityStartPattern()) < 0 : true) && !exitThread )
1132 				line = m_xmlFile.readLine();
1133 			if ( m_xmlFile.atEnd() ) {
1134 				emit progressChanged(m_xmlIndexCount);
1135 				setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1136 				return false;
1137 			} else if ( !exitThread ) {
1138 				QString xmlString;
1139 				if ( rebuilderDialog()->romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
1140 					int startIndex = line.indexOf(listEntityStartPattern());
1141 					if ( startIndex >= 0 ) {
1142 						startIndex += listEntityStartPattern().length();
1143 						setListCheckpoint(line.mid(startIndex, line.indexOf("\"", startIndex) - startIndex), rebuilderDialog()->comboBoxXmlSource->currentIndex());
1144 						return true;
1145 					}
1146 				}
1147 				QString setEntityEndPattern("</" + rebuilderDialog()->lineEditSetEntity->text() + ">");
1148 				while ( !m_xmlFile.atEnd() && line.indexOf(setEntityEndPattern) < 0 && !exitThread ) {
1149 					xmlString += line;
1150 					line = m_xmlFile.readLine();
1151 				}
1152 				if ( !m_xmlFile.atEnd() && !exitThread ) {
1153 					xmlString += line;
1154 					if ( parseXml(xmlString, id, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List, diskSizeList) ) {
1155 						if ( rebuilderDialog()->romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE && !m_currentListName.isEmpty() )
1156 							id->prepend(m_currentListName + ":");
1157 						if ( !evaluateFilters(*id) )
1158 							id->clear();
1159 						m_xmlIndex = m_xmlFile.pos();
1160 						emit progressChanged(m_xmlIndex);
1161 						return true;
1162 					} else {
1163 						emit log(tr("FATAL: XML parsing failed"));
1164 						setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1165 						return false;
1166 					}
1167 				} else {
1168 					emit log(tr("FATAL: XML parsing failed"));
1169 					setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1170 					return false;
1171 				}
1172 			} else {
1173 				setCheckpoint(-1, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1174 				return false;
1175 			}
1176 		}
1177 	}
1178 }
1179 
setCheckpoint(qint64 cp,int xmlSourceIndex)1180 void CollectionRebuilderThread::setCheckpoint(qint64 cp, int xmlSourceIndex)
1181 {
1182 	m_checkpoint = cp;
1183 	if ( xmlSourceIndex == 0 )
1184 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + rebuilderDialog()->settingsKey() + "/Checkpoint", checkpoint());
1185 	else if ( xmlSourceIndex > 0 && xmlSourceIndex < rebuilderDialog()->comboBoxXmlSource->count() - 1 ) {
1186 		xmlSourceIndex -= 1;
1187 		QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + rebuilderDialog()->settingsKey() + "/XmlSourceCheckpoints", QStringList()).toStringList());
1188 		checkpointList.replace(xmlSourceIndex, QString::number(checkpoint()));
1189 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + rebuilderDialog()->settingsKey() + "/XmlSourceCheckpoints", checkpointList);
1190 	}
1191 }
1192 
setListCheckpoint(QString list,int xmlSourceIndex)1193 void CollectionRebuilderThread::setListCheckpoint(QString list, int xmlSourceIndex)
1194 {
1195 	m_currentListName = list;
1196 	if ( xmlSourceIndex > 0 && xmlSourceIndex < rebuilderDialog()->comboBoxXmlSource->count() - 1 ) {
1197 		xmlSourceIndex -= 1;
1198 		QStringList checkpointList(qmc2Config->value(QMC2_FRONTEND_PREFIX + rebuilderDialog()->settingsKey() + "/XmlSourceListCheckpoints", QStringList()).toStringList());
1199 		checkpointList.replace(xmlSourceIndex, list);
1200 		qmc2Config->setValue(QMC2_FRONTEND_PREFIX + rebuilderDialog()->settingsKey() + "/XmlSourceListCheckpoints", checkpointList);
1201 	}
1202 }
1203 
checkpointRestart(qint64 cp)1204 void CollectionRebuilderThread::checkpointRestart(qint64 cp)
1205 {
1206 	if ( cp < 0 ) {
1207 		m_xmlIndex = m_xmlIndexCount = -1;
1208 		return;
1209 	}
1210 	m_xmlIndex = cp;
1211 	if ( rebuilderDialog()->defaultEmulator() ) {
1212 		emit log(tr("restarting from checkpoint '%1'").arg(m_xmlIndex));
1213 		switch ( rebuilderDialog()->romAlyzer()->mode() ) {
1214 			case QMC2_ROMALYZER_MODE_SOFTWARE:
1215 				swlDb()->initIdAtIndexCache();
1216 				m_xmlIndexCount = swlDb()->idAtIndexCacheSize();
1217 				break;
1218 			case QMC2_ROMALYZER_MODE_SYSTEM:
1219 			default:
1220 				xmlDb()->initIdAtIndexCache();
1221 				m_xmlIndexCount = xmlDb()->idAtIndexCacheSize();
1222 				break;
1223 		}
1224 		emit progressRangeChanged(m_xmlIndex, m_xmlIndexCount);
1225 		emit progressChanged(m_xmlIndex);
1226 	} else {
1227 		if ( m_xmlFile.isOpen() )
1228 			m_xmlFile.close();
1229 		m_xmlFile.setFileName(rebuilderDialog()->comboBoxXmlSource->currentText());
1230 		if ( m_xmlFile.open(QIODevice::ReadOnly | QIODevice::Text) ) {
1231 			emit log(tr("restarting from checkpoint '%1'").arg(m_xmlIndex));
1232 			m_xmlIndexCount = m_xmlFile.size() - 1;
1233 			m_xmlFile.seek(m_xmlIndex);
1234 			emit progressRangeChanged(m_xmlIndex, m_xmlIndexCount);
1235 			emit progressChanged(m_xmlIndex);
1236 		} else {
1237 			emit log(tr("FATAL: can't open XML file '%1' for reading, please check permissions").arg(rebuilderDialog()->comboBoxXmlSource->currentText()));
1238 			m_xmlIndex = m_xmlIndexCount = -1;
1239 		}
1240 	}
1241 	setCheckpoint(m_xmlIndex, rebuilderDialog()->comboBoxXmlSource->currentIndex());
1242 }
1243 
rewriteSet(QString * setKey,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList * diskNameList,QStringList * diskSha1List)1244 bool CollectionRebuilderThread::rewriteSet(QString *setKey, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList *diskNameList, QStringList *diskSha1List)
1245 {
1246 	QString set, baseDir = rebuilderDialog()->romAlyzer()->lineEditSetRewriterOutputPath->text();
1247 	if ( rebuilderDialog()->romAlyzer()->mode() == QMC2_ROMALYZER_MODE_SOFTWARE ) {
1248 		QStringList setKeyTokens(setKey->split(':', QString::SkipEmptyParts));
1249 		if ( setKeyTokens.count() < 2 )
1250 			return false;
1251 		else {
1252 			baseDir += "/" + setKeyTokens.at(0);
1253 			set = setKeyTokens.at(1);
1254 		}
1255 	} else
1256 		set = *setKey;
1257 	bool rebuildOkay = true;
1258 	if ( !romNameList->isEmpty() ) {
1259 		switch ( rebuilderDialog()->romAlyzer()->comboBoxSetRewriterReproductionType->currentIndex() ) {
1260 			case QMC2_ROMALYZER_RT_ZIP_BUILTIN:
1261 				rebuildOkay = writeAllZipData(baseDir, set, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List);
1262 				break;
1263 #if defined(QMC2_LIBARCHIVE_ENABLED)
1264 			case QMC2_ROMALYZER_RT_ZIP_LIBARCHIVE:
1265 				rebuildOkay = writeAllArchiveData(baseDir, set, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List);
1266 				break;
1267 #endif
1268 			case QMC2_ROMALYZER_RT_FOLDERS:
1269 				rebuildOkay = writeAllFileData(baseDir, set, romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List);
1270 				break;
1271 			default:
1272 				break;
1273 		}
1274 	}
1275 	if ( rebuildOkay && !diskNameList->isEmpty() ) {
1276 		switch ( rebuilderDialog()->romAlyzer()->comboBoxCollectionRebuilderCHDHandling->currentIndex() ) {
1277 			case QMC2_COLLECTIONREBUILDER_CHD_HARDLINK:
1278 				rebuildOkay = hardlinkChds(baseDir, set, diskNameList, diskSha1List);
1279 				break;
1280 			case QMC2_COLLECTIONREBUILDER_CHD_SYMLINK:
1281 				rebuildOkay = symlinkChds(baseDir, set, diskNameList, diskSha1List);
1282 				break;
1283 			case QMC2_COLLECTIONREBUILDER_CHD_COPY:
1284 				rebuildOkay = copyChds(baseDir, set, diskNameList, diskSha1List);
1285 				break;
1286 			case QMC2_COLLECTIONREBUILDER_CHD_MOVE:
1287 				rebuildOkay = moveChds(baseDir, set, diskNameList, diskSha1List);
1288 				break;
1289 			default:
1290 				break;
1291 		}
1292 	}
1293 	return rebuildOkay;
1294 }
1295 
writeAllFileData(QString baseDir,QString id,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList *,QStringList *)1296 bool CollectionRebuilderThread::writeAllFileData(QString baseDir, QString id, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList * /*diskNameList*/, QStringList * /*diskSha1List*/)
1297 {
1298 	bool success = true;
1299 	QDir d(QDir::cleanPath(baseDir + "/" + id));
1300 	if ( !d.exists() )
1301 		success = d.mkdir(QDir::cleanPath(baseDir + "/" + id));
1302 	int reproducedDumps = 0;
1303 	bool ignoreErrors = !rebuilderDialog()->romAlyzer()->checkBoxSetRewriterAbortOnError->isChecked();
1304 	for (int i = 0; i < romNameList->count() && !exitThread && success; i++) {
1305 		QString fileName(d.absoluteFilePath(romNameList->at(i)));
1306 		if ( !createBackup(fileName) ) {
1307 			emit log(tr("FATAL: backup creation failed"));
1308 			success = false;
1309 		}
1310 		QFile f(fileName);
1311 		QString errorReason(tr("file error"));
1312 		if ( success && f.open(QIODevice::WriteOnly) ) {
1313 			BigByteArray data;
1314 			quint64 size = romSizeList->at(i).toULongLong();
1315 			QString path, member, type;
1316 			if ( checkSumDb()->getData(romSha1List->at(i), romCrcList->at(i), &size, &path, &member, &type) ) {
1317 				switch ( m_fileTypes.indexOf(type) ) {
1318 					case QMC2_COLLECTIONREBUILDER_FILETYPE_ZIP:
1319 						success = readZipFileData(path, romCrcList->at(i), member, &data);
1320 						break;
1321 					case QMC2_COLLECTIONREBUILDER_FILETYPE_7Z:
1322 						success = readSevenZipFileData(path, romCrcList->at(i), member, &data);
1323 						break;
1324 					case QMC2_COLLECTIONREBUILDER_FILETYPE_FILE:
1325 						success = readFileData(path, &data);
1326 						break;
1327 					default:
1328 						success = false;
1329 						errorReason = tr("unknown file type '%1'").arg(type);
1330 						break;
1331 				}
1332 				if ( success ) {
1333 					emit log(tr("writing '%1' (size: %2)").arg(fileName).arg(ROMAlyzer::humanReadable(data.length())));
1334 					quint64 bytesWritten = 0;
1335 					while ( bytesWritten < (quint64)data.length() && !exitThread && success ) {
1336 						quint64 bufferLength = QMC2_ZIP_BUFFER_SIZE;
1337 						if ( bytesWritten + bufferLength > (quint64)data.length() )
1338 							bufferLength = data.length() - bytesWritten;
1339 						qint64 len = f.write(data.mid(bytesWritten, bufferLength));
1340 						success = (len >= 0);
1341 						if ( success ) {
1342 							bytesWritten += len;
1343 						} else
1344 							log(tr("FATAL: failed writing '%1'").arg(fileName));
1345 					}
1346 				}
1347 				f.close();
1348 				reproducedDumps++;
1349 			} else {
1350 				f.close();
1351 				f.remove();
1352 			}
1353 		} else {
1354 			emit log(tr("FATAL: failed opening '%1' for writing"));
1355 			success = false;
1356 		}
1357 		if ( !success ) {
1358 			emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1359 			emit newMissing(id, tr("ROM"), romNameList->at(i), romSizeList->at(i), romCrcList->at(i), romSha1List->at(i), errorReason);
1360 		}
1361 		if ( ignoreErrors )
1362 			success = true;
1363 		else if ( !success ) {
1364 			for (int j = i + 1; j < romNameList->count() && !exitThread; j++) {
1365 				emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1366 				emit newMissing(id, tr("ROM"), romNameList->at(j), romSizeList->at(j), romCrcList->at(j), romSha1List->at(j), errorReason);
1367 			}
1368 		}
1369 	}
1370 	if ( reproducedDumps == 0 )
1371 		d.rmdir(d.absolutePath());
1372 	return success;
1373 }
1374 
writeAllZipData(QString baseDir,QString id,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList *,QStringList *)1375 bool CollectionRebuilderThread::writeAllZipData(QString baseDir, QString id, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList * /*diskNameList*/, QStringList * /*diskSha1List*/)
1376 {
1377 	QDir d(QDir::cleanPath(baseDir));
1378 	if ( !d.exists() )
1379 		if ( !d.mkdir(QDir::cleanPath(baseDir)) )
1380 			return false;
1381 	QString fileName(QDir::cleanPath(baseDir) + "/" + id + ".zip");
1382 	if ( !createBackup(fileName) ) {
1383 		emit log(tr("FATAL: backup creation failed"));
1384 		return false;
1385 	}
1386 	QFile f(fileName);
1387 	if ( f.exists() )
1388 		if ( !f.remove() )
1389 			return false;
1390 	bool success = true;
1391 	bool uniqueCRCs = rebuilderDialog()->romAlyzer()->checkBoxSetRewriterUniqueCRCs->isChecked();
1392 	bool ignoreErrors = !rebuilderDialog()->romAlyzer()->checkBoxSetRewriterAbortOnError->isChecked();
1393 	int zipLevel = rebuilderDialog()->romAlyzer()->spinBoxSetRewriterZipLevel->value();
1394 	zipFile zip = zipOpen(fileName.toUtf8().constData(), APPEND_STATUS_CREATE);
1395 	if ( zip ) {
1396 		emit log(tr("creating new ZIP archive '%1'").arg(fileName));
1397 		zip_fileinfo zipInfo;
1398 		QDateTime cDT = QDateTime::currentDateTime();
1399 		zipInfo.tmz_date.tm_sec = cDT.time().second();
1400 		zipInfo.tmz_date.tm_min = cDT.time().minute();
1401 		zipInfo.tmz_date.tm_hour = cDT.time().hour();
1402 		zipInfo.tmz_date.tm_mday = cDT.date().day();
1403 		zipInfo.tmz_date.tm_mon = cDT.date().month() - 1;
1404 		zipInfo.tmz_date.tm_year = cDT.date().year();
1405 		zipInfo.dosDate = zipInfo.internal_fa = zipInfo.external_fa = 0;
1406 		QStringList storedCRCs;
1407 		int reproducedDumps = 0;
1408 		for (int i = 0; i < romNameList->count() && !exitThread && success; i++) {
1409 			if ( uniqueCRCs && storedCRCs.contains(romCrcList->at(i)) ) {
1410 				emit log(tr("skipping '%1'").arg(romNameList->at(i)) + " ("+ tr("a dump with CRC '%1' already exists").arg(romCrcList->at(i)) + ")");
1411 				continue;
1412 			}
1413 			QString file(romNameList->at(i));
1414 			BigByteArray data;
1415 			quint64 size = romSizeList->at(i).toULongLong();
1416 			QString path, member, type;
1417 			QString errorReason(tr("file error"));
1418 			if ( checkSumDb()->getData(romSha1List->at(i), romCrcList->at(i), &size, &path, &member, &type) ) {
1419 				switch ( m_fileTypes.indexOf(type) ) {
1420 					case QMC2_COLLECTIONREBUILDER_FILETYPE_ZIP:
1421 						success = readZipFileData(path, romCrcList->at(i), member, &data);
1422 						break;
1423 					case QMC2_COLLECTIONREBUILDER_FILETYPE_7Z:
1424 						success = readSevenZipFileData(path, romCrcList->at(i), member, &data);
1425 						break;
1426 					case QMC2_COLLECTIONREBUILDER_FILETYPE_FILE:
1427 						success = readFileData(path, &data);
1428 						break;
1429 					default:
1430 						success = false;
1431 						errorReason = tr("unknown file type '%1'").arg(type);
1432 						break;
1433 				}
1434 				if ( success && zipOpenNewFileInZip(zip, file.toUtf8().constData(), &zipInfo, file.toUtf8().constData(), file.length(), 0, 0, 0, Z_DEFLATED, zipLevel) == ZIP_OK ) {
1435 					emit log(tr("writing '%1' to ZIP archive '%2' (uncompressed size: %3)").arg(file).arg(fileName).arg(ROMAlyzer::humanReadable(data.length())));
1436 					quint64 bytesWritten = 0;
1437 					while ( bytesWritten < (quint64)data.length() && !exitThread && success ) {
1438 						quint64 bufferLength = QMC2_ZIP_BUFFER_SIZE;
1439 						if ( bytesWritten + bufferLength > (quint64)data.length() )
1440 							bufferLength = data.length() - bytesWritten;
1441 						QByteArray writeBuffer(data.mid(bytesWritten, bufferLength));
1442 						success = (zipWriteInFileInZip(zip, (const void *)writeBuffer.data(), bufferLength) == ZIP_OK);
1443 						if ( success )
1444 							bytesWritten += bufferLength;
1445 						else {
1446 							emit log(tr("FATAL: failed writing '%1' to ZIP archive '%2'").arg(file).arg(fileName));
1447 							success = false;
1448 						}
1449 					}
1450 					storedCRCs << romCrcList->at(i);
1451 					zipCloseFileInZip(zip);
1452 					if ( success )
1453 						reproducedDumps++;
1454 				}
1455 			}
1456 			if ( !success ) {
1457 				emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1458 				emit newMissing(id, tr("ROM"), romNameList->at(i), romSizeList->at(i), romCrcList->at(i), romSha1List->at(i), errorReason);
1459 			}
1460 			if ( ignoreErrors )
1461 				success = true;
1462 			else if ( !success ) {
1463 				for (int j = i + 1; j < romNameList->count() && !exitThread; j++) {
1464 					emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1465 					emit newMissing(id, tr("ROM"), romNameList->at(j), romSizeList->at(j), romCrcList->at(j), romSha1List->at(j), errorReason);
1466 				}
1467 			}
1468 		}
1469 		if ( rebuilderDialog()->romAlyzer()->checkBoxSetRewriterAddZipComment->isChecked() )
1470 			zipClose(zip, tr("Created by QMC2 v%1 (%2)").arg(XSTR(QMC2_VERSION)).arg(cDT.toString(Qt::SystemLocaleShortDate)).toUtf8().constData());
1471 		else
1472 			zipClose(zip, "");
1473 		if ( reproducedDumps == 0 )
1474 			f.remove();
1475 		emit log(tr("done (creating new ZIP archive '%1')").arg(fileName));
1476 	} else {
1477 		emit log(tr("FATAL: failed creating ZIP archive '%1'").arg(fileName));
1478 		success = false;
1479 	}
1480 	return success;
1481 }
1482 
1483 #if defined(QMC2_LIBARCHIVE_ENABLED)
writeAllArchiveData(QString baseDir,QString id,QStringList * romNameList,QStringList * romSha1List,QStringList * romCrcList,QStringList * romSizeList,QStringList *,QStringList *)1484 bool CollectionRebuilderThread::writeAllArchiveData(QString baseDir, QString id, QStringList *romNameList, QStringList *romSha1List, QStringList *romCrcList, QStringList *romSizeList, QStringList * /*diskNameList*/, QStringList * /*diskSha1List*/)
1485 {
1486 	QDir d(QDir::cleanPath(baseDir));
1487 	if ( !d.exists() )
1488 		if ( !d.mkdir(QDir::cleanPath(baseDir)) )
1489 			return false;
1490 	QString fileName(QDir::cleanPath(baseDir) + "/" + id + ".zip");
1491 	if ( !createBackup(fileName) ) {
1492 		emit log(tr("FATAL: backup creation failed"));
1493 		return false;
1494 	}
1495 	QFile f(fileName);
1496 	if ( f.exists() )
1497 		if ( !f.remove() )
1498 			return false;
1499 	bool success = true;
1500 	bool uniqueCRCs = rebuilderDialog()->romAlyzer()->checkBoxSetRewriterUniqueCRCs->isChecked();
1501 	bool ignoreErrors = !rebuilderDialog()->romAlyzer()->checkBoxSetRewriterAbortOnError->isChecked();
1502 	ArchiveFile af(fileName, true, rebuilderDialog()->romAlyzer()->comboBoxSetRewriterLibArchiveDeflate->currentIndex() == 0);
1503 	if ( af.open(QIODevice::WriteOnly) ) {
1504 		emit log(tr("creating new ZIP archive '%1'").arg(fileName));
1505 		QStringList storedCRCs;
1506 		int reproducedDumps = 0;
1507 		for (int i = 0; i < romNameList->count() && !exitThread && success; i++) {
1508 			if ( uniqueCRCs && storedCRCs.contains(romCrcList->at(i)) ) {
1509 				emit log(tr("skipping '%1'").arg(romNameList->at(i)) + " ("+ tr("a dump with CRC '%1' already exists").arg(romCrcList->at(i)) + ")");
1510 				continue;
1511 			}
1512 			QString file(romNameList->at(i));
1513 			BigByteArray data;
1514 			quint64 size = romSizeList->at(i).toULongLong();
1515 			QString path, member, type;
1516 			QString errorReason(tr("file error"));
1517 			if ( checkSumDb()->getData(romSha1List->at(i), romCrcList->at(i), &size, &path, &member, &type) ) {
1518 				switch ( m_fileTypes.indexOf(type) ) {
1519 					case QMC2_COLLECTIONREBUILDER_FILETYPE_ZIP:
1520 						success = readZipFileData(path, romCrcList->at(i), member, &data);
1521 						break;
1522 					case QMC2_COLLECTIONREBUILDER_FILETYPE_7Z:
1523 						success = readSevenZipFileData(path, romCrcList->at(i), member, &data);
1524 						break;
1525 					case QMC2_COLLECTIONREBUILDER_FILETYPE_FILE:
1526 						success = readFileData(path, &data);
1527 						break;
1528 					default:
1529 						success = false;
1530 						errorReason = tr("unknown file type '%1'").arg(type);
1531 						break;
1532 				}
1533 				if ( success && af.createEntry(file, data.size()) ) {
1534 					emit log(tr("writing '%1' to ZIP archive '%2' (uncompressed size: %3)").arg(file).arg(fileName).arg(ROMAlyzer::humanReadable(data.length())));
1535 					if ( !af.writeEntryDataBig(data) ) {
1536 						emit log(tr("FATAL: failed writing '%1' to ZIP archive '%2'").arg(file).arg(fileName));
1537 						success = false;
1538 					}
1539 					storedCRCs << romCrcList->at(i);
1540 					af.closeEntry();
1541 					if ( success )
1542 						reproducedDumps++;
1543 				}
1544 			}
1545 			if ( !success ) {
1546 				emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1547 				emit newMissing(id, tr("ROM"), romNameList->at(i), romSizeList->at(i), romCrcList->at(i), romSha1List->at(i), errorReason);
1548 			}
1549 			if ( ignoreErrors )
1550 				success = true;
1551 			else if ( !success ) {
1552 				for (int j = i + 1; j < romNameList->count() && !exitThread; j++) {
1553 					emit statusUpdated(setsProcessed, ++missingROMs, missingDisks);
1554 					emit newMissing(id, tr("ROM"), romNameList->at(j), romSizeList->at(j), romCrcList->at(j), romSha1List->at(j), errorReason);
1555 				}
1556 			}
1557 		}
1558 		af.close();
1559 		if ( reproducedDumps == 0 )
1560 			f.remove();
1561 		emit log(tr("done (creating new ZIP archive '%1')").arg(fileName));
1562 	} else {
1563 		emit log(tr("FATAL: failed creating ZIP archive '%1'").arg(fileName));
1564 		success = false;
1565 	}
1566 	return success;
1567 }
1568 #endif
1569 
readFileData(QString fileName,BigByteArray * data)1570 bool CollectionRebuilderThread::readFileData(QString fileName, BigByteArray *data)
1571 {
1572 	QFile file(fileName);
1573 	data->clear();
1574 	if ( file.open(QIODevice::ReadOnly) ) {
1575   		char ioBuffer[QMC2_ROMALYZER_FILE_BUFFER_SIZE];
1576 		int len = 0;
1577 		emit log(tr("reading '%1' (size: %2)").arg(fileName).arg(ROMAlyzer::humanReadable(file.size())));
1578 		while ( (len = file.read(ioBuffer, QMC2_FILE_BUFFER_SIZE)) > 0 && !exitThread )
1579 			data->append(QByteArray((const char *)ioBuffer, len));
1580 		file.close();
1581 		return true;
1582 	} else {
1583 		emit log(tr("FATAL: failed reading '%1'").arg(fileName));
1584 		return false;
1585 	}
1586 }
1587 
readSevenZipFileData(QString fileName,QString crc,QString member,BigByteArray * data)1588 bool CollectionRebuilderThread::readSevenZipFileData(QString fileName, QString crc, QString member, BigByteArray *data)
1589 {
1590 	SevenZipFile sevenZipFile(fileName);
1591 	if ( sevenZipFile.open() ) {
1592 		int index = sevenZipFile.indexOfCrc(crc);
1593 		if ( index >= 0 ) {
1594 			if ( sevenZipFile.isCrcDuplicate(crc) ) {
1595 				int nameIndex = sevenZipFile.indexOfName(member);
1596 				if ( nameIndex >= 0 )
1597 					index = nameIndex;
1598 			}
1599 			SevenZipMetaData metaData = sevenZipFile.entryList()[index];
1600 			emit log(tr("reading '%1' from 7Z archive '%2' (uncompressed size: %3)").arg(metaData.name()).arg(fileName).arg(ROMAlyzer::humanReadable(metaData.size())));
1601 			quint64 readLength = sevenZipFile.readBig(index, data); // can't be interrupted!
1602 			if ( sevenZipFile.hasError() ) {
1603 				emit log(tr("FATAL: failed reading '%1' from 7Z archive '%2'").arg(metaData.name()).arg(fileName));
1604 				return false;
1605 			}
1606 			if ( readLength != metaData.size() ) {
1607 				emit log(tr("FATAL: failed reading '%1' from 7Z archive '%2'").arg(metaData.name()).arg(fileName));
1608 				return false;
1609 			}
1610 			return true;
1611 		} else {
1612 			emit log(tr("FATAL: failed reading from 7Z archive '%1'").arg(fileName));
1613 			return false;
1614 		}
1615 	} else {
1616 		emit log(tr("FATAL: failed reading from 7Z archive '%1'").arg(fileName));
1617 		return false;
1618 	}
1619 }
1620 
readZipFileData(QString fileName,QString crc,QString member,BigByteArray * data)1621 bool CollectionRebuilderThread::readZipFileData(QString fileName, QString crc, QString member, BigByteArray *data)
1622 {
1623 	bool success = true;
1624 	unzFile zipFile = unzOpen(fileName.toUtf8().constData());
1625 	if ( zipFile ) {
1626   		char ioBuffer[QMC2_ZIP_BUFFER_SIZE];
1627 		unz_file_info zipInfo;
1628 		QMultiMap<uLong, QString> crcIdentMap;
1629 		uLong ulCRC = crc.toULong(0, 16);
1630 		do {
1631 			if ( unzGetCurrentFileInfo(zipFile, &zipInfo, ioBuffer, QMC2_ROMALYZER_ZIP_BUFFER_SIZE, 0, 0, 0, 0) == UNZ_OK )
1632 				crcIdentMap.insert(zipInfo.crc, QString((const char *)ioBuffer));
1633 		} while ( unzGoToNextFile(zipFile) == UNZ_OK );
1634 		unzGoToFirstFile(zipFile);
1635 		if ( crcIdentMap.contains(ulCRC) ) {
1636 			QString fn;
1637 			QStringList names(crcIdentMap.values(ulCRC));
1638 			if ( names.contains(member) )
1639 				fn = member;
1640 			else
1641 				fn = names.at(0);
1642 			if ( unzLocateFile(zipFile, fn.toUtf8().constData(), 0) == UNZ_OK ) {
1643 				if ( unzOpenCurrentFile(zipFile) == UNZ_OK ) {
1644 					if ( unzGetCurrentFileInfo(zipFile, &zipInfo, 0, 0, 0, 0, 0, 0) == UNZ_OK )
1645 						emit log(tr("reading '%1' from ZIP archive '%2' (uncompressed size: %3)").arg(fn).arg(fileName).arg(ROMAlyzer::humanReadable(zipInfo.uncompressed_size)));
1646 					else
1647 						emit log(tr("reading '%1' from ZIP archive '%2' (uncompressed size: %3)").arg(fn).arg(fileName).arg(tr("unknown")));
1648 					qint64 len;
1649 					while ( (len = unzReadCurrentFile(zipFile, ioBuffer, QMC2_ROMALYZER_ZIP_BUFFER_SIZE)) > 0 && !exitThread )
1650 						data->append(QByteArray((const char *)ioBuffer, len));
1651 					unzCloseCurrentFile(zipFile);
1652 				} else {
1653 					emit log(tr("FATAL: failed reading '%1' from ZIP archive '%2'").arg(fn).arg(fileName));
1654 					success = false;
1655 				}
1656 			} else {
1657 				emit log(tr("FATAL: failed reading '%1' from ZIP archive '%2'").arg(fn).arg(fileName));
1658 				success = false;
1659 			}
1660 		} else {
1661 			emit log(tr("FATAL: CRC '%1' not found in ZIP archive '%2'").arg(crc).arg(fileName));
1662 			success = false;
1663 		}
1664 		unzClose(zipFile);
1665 	} else {
1666 		emit log(tr("FATAL: failed reading from ZIP archive '%1'").arg(fileName));
1667 		success = false;
1668 	}
1669 	return success;
1670 }
1671 
hardlinkChds(QString baseDir,QString id,QStringList * diskNameList,QStringList * diskSha1List)1672 bool CollectionRebuilderThread::hardlinkChds(QString baseDir, QString id, QStringList *diskNameList, QStringList *diskSha1List)
1673 {
1674 	QString targetPath(QDir::cleanPath(baseDir) + "/" + id);
1675 	QDir d(targetPath);
1676 	if ( !d.exists() )
1677 		if ( !d.mkdir(QDir::cleanPath(targetPath)) )
1678 			return false;
1679 	bool success = true;
1680 	QString errorReason = tr("file error");
1681 	int reproducedDumps = 0;
1682 	for (int i = 0; i < diskNameList->count() && !exitThread && success; i++) {
1683 		QString fileName(QDir::cleanPath(targetPath) + "/" + diskNameList->at(i) + ".chd");
1684 		quint64 size = 0;
1685 		QString path, member, type;
1686 		if ( checkSumDb()->getData(diskSha1List->at(i), QString(), &size, &path, &member, &type) ) {
1687 			if ( m_fileTypes.indexOf(type) == QMC2_COLLECTIONREBUILDER_FILETYPE_CHD ) {
1688 				if ( !createBackup(fileName) ) {
1689 					emit log(tr("FATAL: backup creation failed"));
1690 					return false;
1691 				}
1692 				if ( !sameFileSystem(path, fileName) ) {
1693 					emit log(tr("WARNING: '%1' and '%2' are NOT on the same file system, hard-linking will not work").arg(path).arg(fileName) + " - " + tr("falling back to copy mode"));
1694 					emit log(tr("copying CHD file '%1' to '%2'").arg(path).arg(fileName));
1695 					QFile targetChd(fileName);
1696 					if ( targetChd.exists() )
1697 						targetChd.remove();
1698 					QFile sourceChd(path);
1699 					if ( sourceChd.open(QIODevice::ReadOnly) ) {
1700 						if ( targetChd.open(QIODevice::WriteOnly) ) {
1701 							char ioBuffer[QMC2_ROMALYZER_FILE_BUFFER_SIZE];
1702 							int count = 0;
1703 							int len = 0;
1704 							while ( success && (len = sourceChd.read(ioBuffer, QMC2_ROMALYZER_FILE_BUFFER_SIZE)) > 0 ) {
1705 								if ( count++ % QMC2_COPY_IO_RESPONSE == 0 )
1706 									qApp->processEvents();
1707 								if ( targetChd.write(ioBuffer, len) != len ) {
1708 									emit log(tr("FATAL: I/O error while writing to '%1'").arg(fileName));
1709 									success = false;
1710 								}
1711 							}
1712 						} else
1713 							success = false;
1714 					} else
1715 						success = false;
1716 				} else {
1717 					emit log(tr("hard-linking CHD file '%1' to '%2'").arg(path).arg(fileName));
1718 					QFile f(fileName);
1719 					if ( f.exists() )
1720 						f.remove();
1721 #if defined(QMC2_OS_WIN)
1722 					success = CreateHardLink((LPCTSTR)fileName.utf16(), (LPCTSTR)path.utf16(), NULL);
1723 #else
1724 					success = ::link(path.toUtf8().constData(), fileName.toUtf8().constData()) == 0;
1725 #endif
1726 				}
1727 				if ( success )
1728 					reproducedDumps++;
1729 				else
1730 					errorReason = tr("failed hard-linking '%1' to '%2'").arg(path).arg(fileName);
1731 			} else {
1732 				success = false;
1733 				errorReason = tr("invalid file type '%1'").arg(type);
1734 				break;
1735 			}
1736 		}
1737 	}
1738 	if ( reproducedDumps == 0 )
1739 		d.rmdir(d.absolutePath());
1740 	return success;
1741 }
1742 
symlinkChds(QString baseDir,QString id,QStringList * diskNameList,QStringList * diskSha1List)1743 bool CollectionRebuilderThread::symlinkChds(QString baseDir, QString id, QStringList *diskNameList, QStringList *diskSha1List)
1744 {
1745 	QString targetPath(QDir::cleanPath(baseDir) + "/" + id);
1746 	QDir d(targetPath);
1747 	if ( !d.exists() )
1748 		if ( !d.mkdir(QDir::cleanPath(targetPath)) )
1749 			return false;
1750 	bool success = true;
1751 	QString errorReason = tr("file error");
1752 	int reproducedDumps = 0;
1753 	for (int i = 0; i < diskNameList->count() && !exitThread && success; i++) {
1754 		QString fileName(QDir::cleanPath(targetPath) + "/" + diskNameList->at(i) + ".chd");
1755 		quint64 size = 0;
1756 		QString path, member, type;
1757 		if ( checkSumDb()->getData(diskSha1List->at(i), QString(), &size, &path, &member, &type) ) {
1758 			if ( m_fileTypes.indexOf(type) == QMC2_COLLECTIONREBUILDER_FILETYPE_CHD ) {
1759 				if ( !createBackup(fileName) ) {
1760 					emit log(tr("FATAL: backup creation failed"));
1761 					return false;
1762 				}
1763 				emit log(tr("sym-linking CHD file '%1' to '%2'").arg(path).arg(fileName));
1764 				QFile f(fileName);
1765 				if ( f.exists() )
1766 					f.remove();
1767 				QFile sourceChd(path);
1768 				success = sourceChd.link(fileName);
1769 				if ( success )
1770 					reproducedDumps++;
1771 				else
1772 					errorReason = tr("failed sym-linking '%1' to '%2'").arg(path).arg(fileName);
1773 			} else {
1774 				success = false;
1775 				errorReason = tr("invalid file type '%1'").arg(type);
1776 				break;
1777 			}
1778 		}
1779 	}
1780 	if ( reproducedDumps == 0 )
1781 		d.rmdir(d.absolutePath());
1782 	return success;
1783 }
1784 
copyChds(QString baseDir,QString id,QStringList * diskNameList,QStringList * diskSha1List)1785 bool CollectionRebuilderThread::copyChds(QString baseDir, QString id, QStringList *diskNameList, QStringList *diskSha1List)
1786 {
1787 	QString targetPath(QDir::cleanPath(baseDir) + "/" + id);
1788 	QDir d(targetPath);
1789 	if ( !d.exists() )
1790 		if ( !d.mkdir(QDir::cleanPath(targetPath)) )
1791 			return false;
1792 	bool success = true;
1793 	QString errorReason = tr("file error");
1794 	int reproducedDumps = 0;
1795 	for (int i = 0; i < diskNameList->count() && !exitThread && success; i++) {
1796 		QString fileName(QDir::cleanPath(targetPath) + "/" + diskNameList->at(i) + ".chd");
1797 		quint64 size = 0;
1798 		QString path, member, type;
1799 		if ( checkSumDb()->getData(diskSha1List->at(i), QString(), &size, &path, &member, &type) ) {
1800 			if ( m_fileTypes.indexOf(type) == QMC2_COLLECTIONREBUILDER_FILETYPE_CHD ) {
1801 				if ( !createBackup(fileName) ) {
1802 					emit log(tr("FATAL: backup creation failed"));
1803 					return false;
1804 				}
1805 				emit log(tr("copying CHD file '%1' to '%2'").arg(path).arg(fileName));
1806 				QFile targetChd(fileName);
1807 				if ( targetChd.exists() )
1808 					targetChd.remove();
1809 				QFile sourceChd(path);
1810 				if ( sourceChd.open(QIODevice::ReadOnly) ) {
1811 					if ( targetChd.open(QIODevice::WriteOnly) ) {
1812 						char ioBuffer[QMC2_ROMALYZER_FILE_BUFFER_SIZE];
1813 						int count = 0;
1814 						int len = 0;
1815 						while ( success && (len = sourceChd.read(ioBuffer, QMC2_ROMALYZER_FILE_BUFFER_SIZE)) > 0 ) {
1816 							if ( count++ % QMC2_COPY_IO_RESPONSE == 0 )
1817 								qApp->processEvents();
1818 							if ( targetChd.write(ioBuffer, len) != len ) {
1819 								emit log(tr("FATAL: I/O error while writing to '%1'").arg(fileName));
1820 								success = false;
1821 							}
1822 						}
1823 					} else
1824 						success = false;
1825 				} else
1826 					success = false;
1827 				if ( success )
1828 					reproducedDumps++;
1829 				else
1830 					errorReason = tr("failed copying '%1' to '%2'").arg(path).arg(fileName);
1831 			} else {
1832 				success = false;
1833 				errorReason = tr("invalid file type '%1'").arg(type);
1834 				break;
1835 			}
1836 		}
1837 	}
1838 	if ( reproducedDumps == 0 )
1839 		d.rmdir(d.absolutePath());
1840 	return success;
1841 }
1842 
moveChds(QString baseDir,QString id,QStringList * diskNameList,QStringList * diskSha1List)1843 bool CollectionRebuilderThread::moveChds(QString baseDir, QString id, QStringList *diskNameList, QStringList *diskSha1List)
1844 {
1845 	QString targetPath(QDir::cleanPath(baseDir) + "/" + id);
1846 	QDir d(targetPath);
1847 	if ( !d.exists() )
1848 		if ( !d.mkdir(QDir::cleanPath(targetPath)) )
1849 			return false;
1850 	bool success = true;
1851 	QString errorReason = tr("file error");
1852 	int reproducedDumps = 0;
1853 	for (int i = 0; i < diskNameList->count() && !exitThread && success; i++) {
1854 		QString fileName(QDir::cleanPath(targetPath) + "/" + diskNameList->at(i) + ".chd");
1855 		quint64 size = 0;
1856 		QString path, member, type;
1857 		if ( checkSumDb()->getData(diskSha1List->at(i), QString(), &size, &path, &member, &type) ) {
1858 			if ( path == fileName )
1859 				continue;
1860 			if ( m_fileTypes.indexOf(type) == QMC2_COLLECTIONREBUILDER_FILETYPE_CHD ) {
1861 				if ( !createBackup(fileName) ) {
1862 					emit log(tr("FATAL: backup creation failed"));
1863 					return false;
1864 				}
1865 				emit log(tr("moving CHD file '%1' to '%2'").arg(path).arg(fileName));
1866 				QFile targetChd(fileName);
1867 				if ( targetChd.exists() )
1868 					targetChd.remove();
1869 				QFile sourceChd(path);
1870 				if ( sameFileSystem(path, fileName) )
1871 					success = QFile::rename(path, fileName);
1872 				else {
1873 					if ( sourceChd.open(QIODevice::ReadOnly) ) {
1874 						if ( targetChd.open(QIODevice::WriteOnly) ) {
1875 							char ioBuffer[QMC2_ROMALYZER_FILE_BUFFER_SIZE];
1876 							int count = 0;
1877 							int len = 0;
1878 							while ( success && (len = sourceChd.read(ioBuffer, QMC2_ROMALYZER_FILE_BUFFER_SIZE)) > 0 ) {
1879 								if ( count++ % QMC2_COPY_IO_RESPONSE == 0 )
1880 									qApp->processEvents();
1881 								if ( targetChd.write(ioBuffer, len) != len ) {
1882 									emit log(tr("FATAL: I/O error while writing to '%1'").arg(fileName));
1883 									success = false;
1884 								}
1885 							}
1886 						} else
1887 							success = false;
1888 					} else
1889 						success = false;
1890 				}
1891 				if ( success ) {
1892 					reproducedDumps++;
1893 					sourceChd.remove();
1894 				} else
1895 					errorReason = tr("failed moving '%1' to '%2'").arg(path).arg(fileName);
1896 			} else {
1897 				success = false;
1898 				errorReason = tr("invalid file type '%1'").arg(type);
1899 				break;
1900 			}
1901 		}
1902 	}
1903 	if ( reproducedDumps == 0 )
1904 		d.rmdir(d.absolutePath());
1905 	return success;
1906 }
1907 
sameFileSystem(QString path1,QString path2)1908 bool CollectionRebuilderThread::sameFileSystem(QString path1, QString path2)
1909 {
1910 	// - path1 and path2 are expected to be fully-qualified file names
1911 	// - the files may or may not exist, however, the folders they belong to MUST exist for this to work
1912 	//   (otherwise stat() returns ENOENT)
1913 	struct stat stat1, stat2;
1914 	stat(QFileInfo(path1).absoluteDir().absolutePath().toUtf8().constData(), &stat1);
1915 	stat(QFileInfo(path2).absoluteDir().absolutePath().toUtf8().constData(), &stat2);
1916 	return stat1.st_dev == stat2.st_dev;
1917 }
1918 
createBackup(QString filePath)1919 bool CollectionRebuilderThread::createBackup(QString filePath)
1920 {
1921 	if ( !rebuilderDialog()->romAlyzer()->checkBoxCreateBackups->isChecked() || rebuilderDialog()->romAlyzer()->lineEditBackupFolder->text().isEmpty() )
1922 		return true;
1923 	QFile sourceFile(filePath);
1924 	if ( !sourceFile.exists() )
1925 		return true;
1926 	QDir backupDir(rebuilderDialog()->romAlyzer()->lineEditBackupFolder->text());
1927 	QFileInfo backupDirInfo(backupDir.absolutePath());
1928 	if ( backupDirInfo.exists() ) {
1929 		if ( backupDirInfo.isWritable() ) {
1930 #if defined(QMC2_OS_WIN)
1931 			QString filePathCopy = filePath;
1932 			QString destinationPath = QDir::cleanPath(QString(backupDir.absolutePath() + "/" + filePathCopy.replace(":", "")));
1933 #else
1934 			QString destinationPath = QDir::cleanPath(backupDir.absolutePath() + "/" + filePath);
1935 #endif
1936 			QFileInfo destinationPathInfo(destinationPath);
1937 			if ( !destinationPathInfo.dir().exists() ) {
1938 				if ( !backupDir.mkpath(destinationPathInfo.dir().absolutePath()) ) {
1939 					emit log(tr("backup") + ": " + tr("FATAL: target path '%1' cannot be created").arg(destinationPathInfo.dir().absolutePath()));
1940 					return false;
1941 				}
1942 			}
1943 			if ( !sourceFile.open(QIODevice::ReadOnly) ) {
1944 				emit log(tr("backup") + ": " + tr("FATAL: source file '%1' cannot be opened for reading").arg(filePath));
1945 				return false;
1946 			}
1947 			QFile destinationFile(destinationPath);
1948 			if ( destinationFile.open(QIODevice::WriteOnly) ) {
1949 				emit log(tr("backup") + ": " + tr("creating backup copy of '%1' as '%2'").arg(filePath).arg(destinationPath));
1950 				char ioBuffer[QMC2_ROMALYZER_FILE_BUFFER_SIZE];
1951 				int count = 0;
1952 				int len = 0;
1953 				bool success = true;
1954 				while ( success && (len = sourceFile.read(ioBuffer, QMC2_ROMALYZER_FILE_BUFFER_SIZE)) > 0 ) {
1955 					if ( count++ % QMC2_BACKUP_IO_RESPONSE == 0 )
1956 						qApp->processEvents();
1957 					if ( destinationFile.write(ioBuffer, len) != len ) {
1958 						emit log(tr("backup") + ": " + tr("FATAL: I/O error while writing to '%1'").arg(destinationPath));
1959 						success = false;
1960 					}
1961 				}
1962 				sourceFile.close();
1963 				destinationFile.close();
1964 				if ( success ) {
1965 					emit log(tr("backup") + ": " + tr("done (creating backup copy of '%1' as '%2')").arg(filePath).arg(destinationPath));
1966 					return true;
1967 				} else
1968 					return false;
1969 			} else {
1970 				emit log(tr("backup") + ": " + tr("FATAL: destination file '%1' cannot be opened for writing").arg(destinationPath));
1971 				sourceFile.close();
1972 				return false;
1973 			}
1974 		} else {
1975 			emit log(tr("backup") + ": " + tr("FATAL: backup folder '%1' isn't writable").arg(backupDir.absolutePath()));
1976 			return false;
1977 		}
1978 	} else {
1979 		emit log(tr("backup") + ": " + tr("FATAL: backup folder '%1' doesn't exist").arg(backupDir.absolutePath()));
1980 		return false;
1981 	}
1982 }
1983 
setFilterExpression(QString expression,int syntax,int type,bool exact)1984 void CollectionRebuilderThread::setFilterExpression(QString expression, int syntax, int type, bool exact)
1985 {
1986 	doFilter = !expression.isEmpty();
1987 	exactMatch = exact;
1988 	isIncludeFilter = (type == 0);
1989 	QRegExp::PatternSyntax ps;
1990 	switch ( syntax ) {
1991 		case 1:
1992 			ps = QRegExp::RegExp2;
1993 			break;
1994 		case 2:
1995 			ps = QRegExp::Wildcard;
1996 			break;
1997 		case 3:
1998 			ps = QRegExp::WildcardUnix;
1999 			break;
2000 		case 4:
2001 			ps = QRegExp::FixedString;
2002 			break;
2003 		case 5:
2004 			ps = QRegExp::W3CXmlSchema11;
2005 			break;
2006 		case 0:
2007 		default:
2008 			ps = QRegExp::RegExp;
2009 			break;
2010 	}
2011 	filterRx = QRegExp(expression, Qt::CaseSensitive, ps);
2012 	if ( doFilter && !filterRx.isValid() ) {
2013 		emit log(tr("WARNING: invalid filter expression '%1' ignored").arg(expression));
2014 		doFilter = false;
2015 	}
2016 }
2017 
setFilterExpressionSoftware(QString expression,int syntax,int type,bool exact)2018 void CollectionRebuilderThread::setFilterExpressionSoftware(QString expression, int syntax, int type, bool exact)
2019 {
2020 	doFilterSoftware = !expression.isEmpty();
2021 	exactMatchSoftware = exact;
2022 	isIncludeFilterSoftware = (type == 0);
2023 	QRegExp::PatternSyntax ps;
2024 	switch ( syntax ) {
2025 		case 1:
2026 			ps = QRegExp::RegExp2;
2027 			break;
2028 		case 2:
2029 			ps = QRegExp::Wildcard;
2030 			break;
2031 		case 3:
2032 			ps = QRegExp::WildcardUnix;
2033 			break;
2034 		case 4:
2035 			ps = QRegExp::FixedString;
2036 			break;
2037 		case 5:
2038 			ps = QRegExp::W3CXmlSchema11;
2039 			break;
2040 		case 0:
2041 		default:
2042 			ps = QRegExp::RegExp;
2043 			break;
2044 	}
2045 	filterRxSoftware = QRegExp(expression, Qt::CaseSensitive, ps);
2046 	if ( doFilterSoftware && !filterRxSoftware.isValid() ) {
2047 		emit log(tr("WARNING: invalid filter expression '%1' ignored").arg(expression));
2048 		doFilterSoftware = false;
2049 	}
2050 }
2051 
pause()2052 void CollectionRebuilderThread::pause()
2053 {
2054 	pauseRequested = true;
2055 }
2056 
resume()2057 void CollectionRebuilderThread::resume()
2058 {
2059 	isPaused = false;
2060 }
2061 
evaluateFilters(QString & setKey)2062 bool CollectionRebuilderThread::evaluateFilters(QString &setKey)
2063 {
2064 	static QString list, set;
2065 
2066 	if ( setKey.isEmpty() )
2067 		return false;
2068 
2069 	switch ( rebuilderDialog()->romAlyzer()->mode() ) {
2070 		case QMC2_ROMALYZER_MODE_SOFTWARE: {
2071 				QStringList setKeyTokens(setKey.split(':', QString::SkipEmptyParts));
2072 				if ( setKeyTokens.count() < 2 )
2073 					return false;
2074 				list = setKeyTokens.at(0);
2075 				if ( doFilterSoftware ) {
2076 					if ( isIncludeFilterSoftware ) {
2077 						if ( exactMatchSoftware ) {
2078 							if ( !filterRxSoftware.exactMatch(list) )
2079 								return false;
2080 						} else if ( filterRxSoftware.indexIn(list) < 0 )
2081 							return false;
2082 					} else {
2083 						if ( exactMatchSoftware ) {
2084 							if ( filterRxSoftware.exactMatch(list) )
2085 								return false;
2086 						} else if ( filterRxSoftware.indexIn(list) >= 0 )
2087 							return false;
2088 					}
2089 				}
2090 				set = setKeyTokens.at(1);
2091 				break;
2092 			}
2093 		case QMC2_ROMALYZER_MODE_SYSTEM:
2094 		default:
2095 			set = setKey;
2096 			if ( doFilterState ) {
2097 				switch ( qmc2MachineList->romState(set) ) {
2098 					case 'C':
2099 						if ( !includeStateC )
2100 							return false;
2101 						break;
2102 					case 'M':
2103 						if ( !includeStateM )
2104 							return false;
2105 						break;
2106 					case 'I':
2107 						if ( !includeStateI )
2108 							return false;
2109 						break;
2110 					case 'N':
2111 						if ( !includeStateN )
2112 							return false;
2113 						break;
2114 					case 'U':
2115 					default:
2116 						if ( !includeStateU )
2117 							return false;
2118 						break;
2119 				}
2120 			}
2121 			break;
2122 	}
2123 	if ( doFilter ) {
2124 		if ( isIncludeFilter ) {
2125 			if ( exactMatch ) {
2126 				if ( !filterRx.exactMatch(set) )
2127 					return false;
2128 			} else if ( filterRx.indexIn(set) < 0 )
2129 				return false;
2130 		} else {
2131 			if ( exactMatch ) {
2132 				if ( filterRx.exactMatch(set) )
2133 					return false;
2134 			} else if ( filterRx.indexIn(set) >= 0 )
2135 				return false;
2136 		}
2137 	}
2138 	return true;
2139 }
2140 
checkSumExists(QString sha1,QString crc,quint64 size)2141 bool CollectionRebuilderThread::checkSumExists(QString sha1, QString crc, quint64 size)
2142 {
2143 	if ( useHashCache ) {
2144 		if ( sha1.isEmpty() ) {
2145 			if ( m_hashCache.contains(QString("-%1-%2").arg(crc).arg(size)) )
2146 				return true;
2147 			else { // rare case so shouldn't hurt
2148 				QStringList uniqueKeys(m_hashCache.uniqueKeys());
2149 				return uniqueKeys.indexOf(QRegExp(QString(".*-%1-%2").arg(crc).arg(size))) >= 0;
2150 			}
2151 		} else {
2152 			if ( m_hashCache.contains(QString("%1-%2-%3").arg(sha1).arg(crc).arg(size)) )
2153 				return true;
2154 			else
2155 				return m_hashCache.contains(QString("-%1-%2").arg(crc).arg(size));
2156 		}
2157 	} else
2158 		return checkSumDb()->exists(sha1, crc, size);
2159 }
2160 
updateHashCache()2161 void CollectionRebuilderThread::updateHashCache()
2162 {
2163 	if ( checkSumDb()->scanTime() > m_hashCacheUpdateTime ) {
2164 		qint64 row = checkSumDb()->nextRowId(true);
2165 		emit progressTextChanged(tr("Preparing"));
2166 		emit progressRangeChanged(0, checkSumDb()->checkSumRowCount() - 1);
2167 		emit progressChanged(0);
2168 		int count = 0;
2169 		emit log(tr("updating hash cache"));
2170 		m_hashCache.clear();
2171 		while ( row > 0 && !exitThread && !stopRebuilding ) {
2172 			emit progressChanged(count++);
2173 			QString key;
2174 			if ( !checkSumDb()->pathOfRow(row, &key, true).isEmpty() )
2175 				m_hashCache.insert(key, true);
2176 			row = checkSumDb()->nextRowId();
2177 			if ( exitThread || stopRebuilding )
2178 				break;
2179 		}
2180 		if ( exitThread || stopRebuilding ) {
2181 			m_hashCacheUpdateTime = 0;
2182 			m_hashCache.clear();
2183 			emit log(tr("hash cache update interrupted"));
2184 		} else {
2185 			emit log(tr("hash cache updated") + " - " + tr("%n hash(es) loaded", "", m_hashCache.count()));
2186 			m_hashCacheUpdateTime = QDateTime::currentDateTime().toTime_t();
2187 			emit progressRangeChanged(m_xmlIndex, m_xmlIndexCount);
2188 			emit progressChanged(m_xmlIndex);
2189 		}
2190 		emit progressChanged(0);
2191 	}
2192 }
2193 
run()2194 void CollectionRebuilderThread::run()
2195 {
2196 	emit log(tr("rebuilder thread started"));
2197 	while ( !exitThread ) {
2198 		emit log(tr("waiting for work"));
2199 		mutex.lock();
2200 		isWaiting = true;
2201 		isActive = isPaused = stopRebuilding = false;
2202 		waitCondition.wait(&mutex);
2203 		isActive = true;
2204 		isWaiting = false;
2205 		mutex.unlock();
2206 		if ( !exitThread && !stopRebuilding ) {
2207 			setsProcessed = missingROMs = missingDisks = 0;
2208 			setSetEntityPattern("<" + rebuilderDialog()->lineEditSetEntity->text() + " name=\"");
2209 			setRomEntityPattern("<" + rebuilderDialog()->lineEditRomEntity->text() + " name=\"");
2210 			setDiskEntityPattern("<" + rebuilderDialog()->lineEditDiskEntity->text() + " name=\"");
2211 			setSetEntityStartPattern("<" + rebuilderDialog()->lineEditSetEntity->text() + " name=\"");
2212 			setMerge(!rebuilderDialog()->romAlyzer()->checkBoxSetRewriterSelfContainedSets->isChecked());
2213 			if ( dryRun )
2214 				emit log(tr("dry run started"));
2215 			else
2216 				emit log(tr("rebuilding started"));
2217 			emit statusUpdated(0, 0, 0);
2218 			emit rebuildStarted();
2219 			QTime rebuildTimer, elapsedTime(0, 0, 0, 0);
2220 			rebuildTimer.start();
2221 			if ( useHashCache )
2222 				updateHashCache();
2223 			if ( dryRun )
2224 				emit progressTextChanged(tr("Analyzing"));
2225 			else
2226 				emit progressTextChanged(tr("Rebuilding"));
2227 			if ( checkpoint() < 0 )
2228 				m_xmlIndex = m_xmlIndexCount = -1;
2229 			QString setKey;
2230 			QStringList romNameList, romSha1List, romCrcList, romSizeList, diskNameList, diskSha1List, diskSizeList;
2231 			while ( !exitThread && !stopRebuilding && nextId(&setKey, &romNameList, &romSha1List, &romCrcList, &romSizeList, &diskNameList, &diskSha1List, &diskSizeList) ) {
2232 				bool pauseMessageLogged = false;
2233 				while ( (pauseRequested || isPaused) && !exitThread && !stopRebuilding ) {
2234 					if ( !pauseMessageLogged ) {
2235 						pauseMessageLogged = true;
2236 						isPaused = true;
2237 						pauseRequested = false;
2238 						emit progressTextChanged(tr("Paused"));
2239 						emit rebuildPaused();
2240 						if ( dryRun )
2241 							emit log(tr("dry run paused"));
2242 						else
2243 							emit log(tr("rebuilding paused"));
2244 					}
2245 					QTest::qWait(100);
2246 				}
2247 				if ( pauseMessageLogged && !exitThread && !stopRebuilding ) {
2248 					isPaused = false;
2249 					if ( dryRun )
2250 						emit progressTextChanged(tr("Analyzing"));
2251 					else
2252 						emit progressTextChanged(tr("Rebuilding"));
2253 					emit rebuildResumed();
2254 					if ( dryRun )
2255 						emit log(tr("dry run resumed"));
2256 					else
2257 						emit log(tr("rebuilding resumed"));
2258 				}
2259 				if ( setKey.isEmpty() )
2260 					continue;
2261 				if ( !exitThread && !stopRebuilding && (!romNameList.isEmpty() || !diskNameList.isEmpty()) ) {
2262 					if ( !dryRun )
2263 						emit log(tr("set rebuilding started for '%1'").arg(setKey));
2264 					for (int i = 0; i < romNameList.count(); i++) {
2265 						bool dbStatusGood = checkSumExists(romSha1List[i], romCrcList[i], romSizeList[i].toULongLong());
2266 						if ( !dryRun )
2267 							emit log(tr("required ROM") + ": " + tr("name = '%1', crc = '%2', sha1 = '%3', database status = '%4'").arg(romNameList[i]).arg(romCrcList[i]).arg(romSha1List[i]).arg(dbStatusGood ? tr("available") : tr("not available")));
2268 						if ( !dbStatusGood ) {
2269 							missingROMs++;
2270 							emit newMissing(setKey, tr("ROM"), romNameList[i], romSizeList[i], romCrcList[i], romSha1List[i], tr("check-sum not available in database"));
2271 						}
2272 					}
2273 					for (int i = 0; i < diskNameList.count(); i++) {
2274 						bool dbStatusGood = checkSumExists(diskSha1List[i], useHashCache ? "0" : QString());
2275 						if ( !dryRun )
2276 							emit log(tr("required disk") + ": " + tr("name = '%1', sha1 = '%2', database status = '%3'").arg(diskNameList[i]).arg(diskSha1List[i]).arg(dbStatusGood ? tr("available") : tr("not available")));
2277 						if ( !dbStatusGood ) {
2278 							missingDisks++;
2279 							emit newMissing(setKey, tr("DISK"), diskNameList[i], diskSizeList[i], QString(), diskSha1List[i], tr("check-sum not available in database"));
2280 						}
2281 					}
2282 					emit statusUpdated(setsProcessed, missingROMs, missingDisks);
2283 					if ( !dryRun ) {
2284 						bool rewriteOkay = true;
2285 						if ( !romNameList.isEmpty() || !diskNameList.isEmpty() )
2286 							rewriteOkay = rewriteSet(&setKey, &romNameList, &romSha1List, &romCrcList, &romSizeList, &diskNameList, &diskSha1List);
2287 						if ( rewriteOkay )
2288 							emit log(tr("set rebuilding finished for '%1'").arg(setKey));
2289 						else
2290 							emit log(tr("set rebuilding failed for '%1'").arg(setKey));
2291 					}
2292 					emit statusUpdated(++setsProcessed, missingROMs, missingDisks);
2293 					setCheckpoint(m_xmlIndex, rebuilderDialog()->comboBoxXmlSource->currentIndex());
2294 					if ( !dryRun ) {
2295 						QTest::qWait(1);
2296 						yieldCurrentThread();
2297 					}
2298 				}
2299 			}
2300 			if ( rebuilderDialog()->defaultEmulator() ) {
2301 				if ( xmlDb() )
2302 					xmlDb()->clearIdAtIndexCache();
2303 				if ( swlDb() )
2304 					swlDb()->clearIdAtIndexCache();
2305 			}
2306 			elapsedTime = elapsedTime.addMSecs(rebuildTimer.elapsed());
2307 			if ( dryRun )
2308 				emit log(tr("dry run finished - total analysis time = %1, sets processed = %2, missing ROMs = %3, missing disks = %4").arg(elapsedTime.toString("hh:mm:ss.zzz")).arg(setsProcessed).arg(missingROMs).arg(missingDisks));
2309 			else
2310 				emit log(tr("rebuilding finished - total rebuild time = %1, sets processed = %2, missing ROMs = %3, missing disks = %4").arg(elapsedTime.toString("hh:mm:ss.zzz")).arg(setsProcessed).arg(missingROMs).arg(missingDisks));
2311 			emit progressRangeChanged(0, 100);
2312 			emit progressChanged(0);
2313 			emit progressTextChanged(tr("Idle"));
2314 			if ( m_xmlFile.isOpen() )
2315 				m_xmlFile.close();
2316 			emit rebuildFinished();
2317 		}
2318 	}
2319 	emit log(tr("rebuilder thread ended"));
2320 }
2321