1 /*
2     SPDX-FileCopyrightText: 2008-2010 Volker Lanz <vl@fidra.de>
3     SPDX-FileCopyrightText: 2008 Laurent Montel <montel@kde.org>
4     SPDX-FileCopyrightText: 2014-2020 Andrius Štikonas <andrius@stikonas.eu>
5     SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
6 
7     SPDX-License-Identifier: GPL-3.0-or-later
8 */
9 
10 #include "core/operationstack.h"
11 #include "core/device.h"
12 #include "core/partition.h"
13 #include "core/partitiontable.h"
14 
15 #include "ops/operation.h"
16 #include "ops/deleteoperation.h"
17 #include "ops/newoperation.h"
18 #include "ops/resizeoperation.h"
19 #include "ops/copyoperation.h"
20 #include "ops/restoreoperation.h"
21 #include "ops/createfilesystemoperation.h"
22 #include "ops/setpartflagsoperation.h"
23 #include "ops/setfilesystemlabeloperation.h"
24 #include "ops/createpartitiontableoperation.h"
25 #include "ops/resizevolumegroupoperation.h"
26 #include "ops/checkoperation.h"
27 
28 #include "jobs/setfilesystemlabeljob.h"
29 
30 #include "fs/filesystemfactory.h"
31 
32 #include "util/globallog.h"
33 
34 #include <KLocalizedString>
35 
36 #include <QReadLocker>
37 #include <QWriteLocker>
38 
39 /** Constructs a new OperationStack */
OperationStack(QObject * parent)40 OperationStack::OperationStack(QObject* parent) :
41     QObject(parent),
42     m_Operations(),
43     m_PreviewDevices(),
44     m_Lock(QReadWriteLock::Recursive)
45 {
46 }
47 
48 /** Destructs an OperationStack, cleaning up Operations and Devices */
~OperationStack()49 OperationStack::~OperationStack()
50 {
51     clearOperations();
52     clearDevices();
53 }
54 
55 /** Tries to merge an existing NewOperation with a new Operation pushed on the OperationStack
56 
57     There are several cases what might need to be done:
58 
59     <ol>
60     <!-- 1 -->
61     <li>An existing operation created a Partition that is now being deleted: In this case, just remove
62         the corresponding NewOperation from the OperationStack.<br/>This does not work for
63         extended partitions.(#232092)</li>
64     <!-- 2 -->
65     <li>An existing Operation created a Partition that is now being moved or resized. In this case,
66         remove the original NewOperation and create a new NewOperation with updated start and end
67         sectors. This new NewOperation is appended to the OperationStack.<br/>This does not work for
68         extended partitions.(#232092)</li>
69     <!-- 3 -->
70     <li>An existing NewOperation created a Partition that is now being copied. We're not copying
71         but instead creating another new Partition in its place.</li>
72     <!-- 4 -->
73     <li>The label for a new Partition's FileSystem is modified: Modify in NewOperation and forget it.</li>
74     <!-- 5 -->
75     <li>File system is changed for a new Partition: Modify in NewOperation and forget it.</li>
76     <!-- 6 -->
77     <li>A file system on a new Partition is about to be checked: Just delete the CheckOperation, because
78         file systems are checked anyway when they're created. This fixes #275657.</li>
79     </ol>
80 
81     @param currentOp the Operation already on the stack to try to merge with
82     @param pushedOp the newly pushed Operation
83     @return true if the OperationStack has been modified in a way that requires merging to stop
84 */
mergeNewOperation(Operation * & currentOp,Operation * & pushedOp)85 bool OperationStack::mergeNewOperation(Operation*& currentOp, Operation*& pushedOp)
86 {
87     NewOperation* newOp = dynamic_cast<NewOperation*>(currentOp);
88 
89     if (newOp == nullptr)
90         return false;
91 
92     DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
93     ResizeOperation* pushedResizeOp = dynamic_cast<ResizeOperation*>(pushedOp);
94     CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
95     SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
96     CreateFileSystemOperation* pushedCreateFileSystemOp = dynamic_cast<CreateFileSystemOperation*>(pushedOp);
97     CheckOperation* pushedCheckOp = dynamic_cast<CheckOperation*>(pushedOp);
98 
99     // -- 1 --
100     if (pushedDeleteOp && &newOp->newPartition() == &pushedDeleteOp->deletedPartition() && !pushedDeleteOp->deletedPartition().roles().has(PartitionRole::Extended)) {
101         Log() << xi18nc("@info:status", "Deleting a partition just created: Undoing the operation to create the partition.");
102 
103         delete pushedOp;
104         pushedOp = nullptr;
105 
106         newOp->undo();
107         delete operations().takeAt(operations().indexOf(newOp));
108 
109         return true;
110     }
111 
112     // -- 2 --
113     if (pushedResizeOp && &newOp->newPartition() == &pushedResizeOp->partition() && !pushedResizeOp->partition().roles().has(PartitionRole::Extended)) {
114         // NOTE: In theory it would be possible to merge resizing an extended as long as it has no children.
115         // But that still doesn't save us: If we're not merging a resize on an extended that has children,
116         // a resizeop is added to the stack. Next, the user deletes the child. Then he resizes the
117         // extended again (a second resize): The ResizeOp still has the pointer to the original extended that
118         // will now be deleted.
119         Log() << xi18nc("@info:status", "Resizing a partition just created: Updating start and end in existing operation.");
120 
121         Partition* newPartition = new Partition(newOp->newPartition());
122         newPartition->setFirstSector(pushedResizeOp->newFirstSector());
123         newPartition->fileSystem().setFirstSector(pushedResizeOp->newFirstSector());
124         newPartition->setLastSector(pushedResizeOp->newLastSector());
125         newPartition->fileSystem().setLastSector(pushedResizeOp->newLastSector());
126 
127         NewOperation* revisedNewOp = new NewOperation(newOp->targetDevice(), newPartition);
128         delete pushedOp;
129         pushedOp = revisedNewOp;
130 
131         newOp->undo();
132         delete operations().takeAt(operations().indexOf(newOp));
133 
134         return true;
135     }
136 
137     // -- 3 --
138     if (pushedCopyOp && &newOp->newPartition() == &pushedCopyOp->sourcePartition()) {
139         Log() << xi18nc("@info:status", "Copying a new partition: Creating a new partition instead.");
140 
141         Partition* newPartition = new Partition(newOp->newPartition());
142         newPartition->setFirstSector(pushedCopyOp->copiedPartition().firstSector());
143         newPartition->fileSystem().setFirstSector(pushedCopyOp->copiedPartition().fileSystem().firstSector());
144         newPartition->setLastSector(pushedCopyOp->copiedPartition().lastSector());
145         newPartition->fileSystem().setLastSector(pushedCopyOp->copiedPartition().fileSystem().lastSector());
146 
147         NewOperation* revisedNewOp = new NewOperation(pushedCopyOp->targetDevice(), newPartition);
148         delete pushedOp;
149         pushedOp = revisedNewOp;
150 
151         return true;
152     }
153 
154     // -- 4 --
155     if (pushedLabelOp && &newOp->newPartition() == &pushedLabelOp->labeledPartition()) {
156         Log() << xi18nc("@info:status", "Changing label for a new partition: No new operation required.");
157 
158         newOp->setLabelJob()->setLabel(pushedLabelOp->newLabel());
159         newOp->newPartition().fileSystem().setLabel(pushedLabelOp->newLabel());
160 
161         delete pushedOp;
162         pushedOp = nullptr;
163 
164         return true;
165     }
166 
167     // -- 5 --
168     if (pushedCreateFileSystemOp && &newOp->newPartition() == &pushedCreateFileSystemOp->partition()) {
169         Log() << xi18nc("@info:status", "Changing file system for a new partition: No new operation required.");
170 
171         FileSystem* oldFs = &newOp->newPartition().fileSystem();
172 
173         newOp->newPartition().setFileSystem(FileSystemFactory::cloneWithNewType(pushedCreateFileSystemOp->newFileSystem()->type(), *oldFs));
174 
175         delete oldFs;
176         oldFs = nullptr;
177 
178         delete pushedOp;
179         pushedOp = nullptr;
180 
181         return true;
182     }
183 
184     // -- 6 --
185     if (pushedCheckOp && &newOp->newPartition() == &pushedCheckOp->checkedPartition()) {
186         Log() << xi18nc("@info:status", "Checking file systems is automatically done when creating them: No new operation required.");
187 
188         delete pushedOp;
189         pushedOp = nullptr;
190 
191         return true;
192     }
193 
194     return false;
195 }
196 
197 /** Tries to merge an existing CopyOperation with a new Operation pushed on the OperationStack.
198 
199     These are the cases to consider:
200 
201     <ol>
202     <!-- 1 -->
203     <li>An existing CopyOperation created a Partition that is now being deleted. Remove the
204         CopyOperation, and, if the CopyOperation was an overwrite, carry on with the delete. Else
205         also remove the DeleteOperation.</li>
206 
207     <!-- 2 -->
208     <li>An existing CopyOperation created a Partition that is now being copied. We're not copying
209         the target of this existing CopyOperation, but its source instead. If this merge is not done,
210         copied partitions will have misleading labels ("copy of sdXY" instead of "copy of copy of
211         sdXY" for a second-generation copy) but the Operation itself will still work.</li>
212     </ol>
213 
214     @param currentOp the Operation already on the stack to try to merge with
215     @param pushedOp the newly pushed Operation
216     @return true if the OperationStack has been modified in a way that requires merging to stop
217 */
mergeCopyOperation(Operation * & currentOp,Operation * & pushedOp)218 bool OperationStack::mergeCopyOperation(Operation*& currentOp, Operation*& pushedOp)
219 {
220     CopyOperation* copyOp = dynamic_cast<CopyOperation*>(currentOp);
221 
222     if (copyOp == nullptr)
223         return false;
224 
225     DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
226     CopyOperation* pushedCopyOp = dynamic_cast<CopyOperation*>(pushedOp);
227 
228     // -- 1 --
229     if (pushedDeleteOp && &copyOp->copiedPartition() == &pushedDeleteOp->deletedPartition()) {
230         // If the copy operation didn't overwrite, but create a new partition, just remove the
231         // copy operation, forget the delete and be done.
232         if (copyOp->overwrittenPartition() == nullptr) {
233             Log() << xi18nc("@info:status", "Deleting a partition just copied: Removing the copy.");
234 
235             delete pushedOp;
236             pushedOp = nullptr;
237         } else {
238             Log() << xi18nc("@info:status", "Deleting a partition just copied over an existing partition: Removing the copy and deleting the existing partition.");
239 
240             pushedDeleteOp->setDeletedPartition(copyOp->overwrittenPartition());
241         }
242 
243         copyOp->undo();
244         delete operations().takeAt(operations().indexOf(copyOp));
245 
246         return true;
247     }
248 
249     // -- 2 --
250     if (pushedCopyOp && &copyOp->copiedPartition() == &pushedCopyOp->sourcePartition()) {
251         Log() << xi18nc("@info:status", "Copying a partition that is itself a copy: Copying the original source partition instead.");
252 
253         pushedCopyOp->setSourcePartition(&copyOp->sourcePartition());
254     }
255 
256     return false;
257 }
258 
259 /** Tries to merge an existing RestoreOperation with a new Operation pushed on the OperationStack.
260 
261     If an existing RestoreOperation created a Partition that is now being deleted, remove the
262     RestoreOperation, and, if the RestoreOperation was an overwrite, carry on with the delete. Else
263     also remove the DeleteOperation.
264 
265     @param currentOp the Operation already on the stack to try to merge with
266     @param pushedOp the newly pushed Operation
267     @return true if the OperationStack has been modified in a way that requires merging to stop
268 */
mergeRestoreOperation(Operation * & currentOp,Operation * & pushedOp)269 bool OperationStack::mergeRestoreOperation(Operation*& currentOp, Operation*& pushedOp)
270 {
271     RestoreOperation* restoreOp = dynamic_cast<RestoreOperation*>(currentOp);
272 
273     if (restoreOp == nullptr)
274         return false;
275 
276     DeleteOperation* pushedDeleteOp = dynamic_cast<DeleteOperation*>(pushedOp);
277 
278     if (pushedDeleteOp && &restoreOp->restorePartition() == &pushedDeleteOp->deletedPartition()) {
279         if (restoreOp->overwrittenPartition() == nullptr) {
280             Log() << xi18nc("@info:status", "Deleting a partition just restored: Removing the restore operation.");
281 
282             delete pushedOp;
283             pushedOp = nullptr;
284         } else {
285             Log() << xi18nc("@info:status", "Deleting a partition just restored to an existing partition: Removing the restore operation and deleting the existing partition.");
286 
287             pushedDeleteOp->setDeletedPartition(restoreOp->overwrittenPartition());
288         }
289 
290         restoreOp->undo();
291         delete operations().takeAt(operations().indexOf(restoreOp));
292 
293         return true;
294     }
295 
296     return false;
297 }
298 
299 /** Tries to merge an existing SetPartFlagsOperation with a new Operation pushed on the OperationStack.
300 
301     If the Partition flags for an existing Partition are modified look if there is an existing
302     Operation for the same Partition and modify that one.
303 
304     @param currentOp the Operation already on the stack to try to merge with
305     @param pushedOp the newly pushed Operation
306     @return true if the OperationStack has been modified in a way that requires merging to stop
307 */
mergePartFlagsOperation(Operation * & currentOp,Operation * & pushedOp)308 bool OperationStack::mergePartFlagsOperation(Operation*& currentOp, Operation*& pushedOp)
309 {
310     SetPartFlagsOperation* partFlagsOp = dynamic_cast<SetPartFlagsOperation*>(currentOp);
311 
312     if (partFlagsOp == nullptr)
313         return false;
314 
315     SetPartFlagsOperation* pushedFlagsOp = dynamic_cast<SetPartFlagsOperation*>(pushedOp);
316 
317     if (pushedFlagsOp && &partFlagsOp->flagPartition() == &pushedFlagsOp->flagPartition()) {
318         Log() << xi18nc("@info:status", "Changing flags again for the same partition: Removing old operation.");
319 
320         pushedFlagsOp->setOldFlags(partFlagsOp->oldFlags());
321         partFlagsOp->undo();
322         delete operations().takeAt(operations().indexOf(partFlagsOp));
323 
324         return true;
325     }
326 
327     return false;
328 }
329 
330 /** Tries to merge an existing SetFileSystemLabelOperation with a new Operation pushed on the OperationStack.
331 
332     If a FileSystem label for an existing Partition is modified look if there is an existing
333     SetFileSystemLabelOperation for the same Partition.
334 
335     @param currentOp the Operation already on the stack to try to merge with
336     @param pushedOp the newly pushed Operation
337     @return true if the OperationStack has been modified in a way that requires merging to stop
338 */
mergePartLabelOperation(Operation * & currentOp,Operation * & pushedOp)339 bool OperationStack::mergePartLabelOperation(Operation*& currentOp, Operation*& pushedOp)
340 {
341     SetFileSystemLabelOperation* partLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(currentOp);
342 
343     if (partLabelOp == nullptr)
344         return false;
345 
346     SetFileSystemLabelOperation* pushedLabelOp = dynamic_cast<SetFileSystemLabelOperation*>(pushedOp);
347 
348     if (pushedLabelOp && &partLabelOp->labeledPartition() == &pushedLabelOp->labeledPartition()) {
349         Log() << xi18nc("@info:status", "Changing label again for the same partition: Removing old operation.");
350 
351         pushedLabelOp->setOldLabel(partLabelOp->oldLabel());
352         partLabelOp->undo();
353         delete operations().takeAt(operations().indexOf(partLabelOp));
354 
355         return true;
356     }
357 
358     return false;
359 }
360 
361 /** Tries to merge an existing CreatePartitionTableOperation with a new Operation pushed on the OperationStack.
362 
363     If a new partition table is to be created on a device and a previous operation targets that
364     device, remove this previous operation.
365 
366     @param currentOp the Operation already on the stack to try to merge with
367     @param pushedOp the newly pushed Operation
368     @return true if the OperationStack has been modified in a way that requires merging to stop
369 */
mergeCreatePartitionTableOperation(Operation * & currentOp,Operation * & pushedOp)370 bool OperationStack::mergeCreatePartitionTableOperation(Operation*& currentOp, Operation*& pushedOp)
371 {
372     CreatePartitionTableOperation* pushedCreatePartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(pushedOp);
373 
374     if (pushedCreatePartitionTableOp && currentOp->targets(pushedCreatePartitionTableOp->targetDevice())) {
375         Log() << xi18nc("@info:status", "Creating new partition table, discarding previous operation on device.");
376 
377         CreatePartitionTableOperation* createPartitionTableOp = dynamic_cast<CreatePartitionTableOperation*>(currentOp);
378         if (createPartitionTableOp != nullptr)
379             pushedCreatePartitionTableOp->setOldPartitionTable(createPartitionTableOp->oldPartitionTable());
380 
381         currentOp->undo();
382         delete operations().takeAt(operations().indexOf(currentOp));
383 
384         return true;
385     }
386 
387     return false;
388 }
389 
mergeResizeVolumeGroupResizeOperation(Operation * & pushedOp)390 bool OperationStack::mergeResizeVolumeGroupResizeOperation(Operation*& pushedOp)
391 {
392     ResizeVolumeGroupOperation* pushedResizeVolumeGroupOp = dynamic_cast<ResizeVolumeGroupOperation*>(pushedOp);
393 
394     if (pushedResizeVolumeGroupOp && pushedResizeVolumeGroupOp->jobs().count() == 0) {
395         Log() << xi18nc("@info:status", "Resizing Volume Group, nothing to do.");
396 
397         return true;
398     }
399 
400     return false;
401 }
402 
403 /** Pushes a new Operation on the OperationStack.
404 
405     This method will call all methods that try to merge the new Operation with the
406     existing ones. It is not uncommon that any of these will delete the pushed
407     Operation. Callers <b>must not rely</b> on the pushed Operation to exist after
408     calling OperationStack::push().
409 
410     @param o Pointer to the Operation. Must not be nullptr.
411 */
push(Operation * o)412 void OperationStack::push(Operation* o)
413 {
414     Q_ASSERT(o);
415 
416     if (mergeResizeVolumeGroupResizeOperation(o))
417         return;
418 
419     for (auto currentOp = operations().rbegin(); currentOp != operations().rend(); ++currentOp) {
420         if (mergeNewOperation(*currentOp, o))
421             break;
422 
423         if (mergeCopyOperation(*currentOp, o))
424             break;
425 
426         if (mergeRestoreOperation(*currentOp, o))
427             break;
428 
429         if (mergePartFlagsOperation(*currentOp, o))
430             break;
431 
432         if (mergePartLabelOperation(*currentOp, o))
433             break;
434 
435         if (mergeCreatePartitionTableOperation(*currentOp, o))
436             break;
437     }
438 
439     if (o != nullptr) {
440         Log() << xi18nc("@info:status", "Add operation: %1", o->description());
441         operations().append(o);
442         o->preview();
443         o->setStatus(Operation::StatusPending);
444     }
445 
446     // Q_EMIT operationsChanged even if o is nullptr because it has been merged: merging might
447     // have led to an existing operation changing.
448     Q_EMIT operationsChanged();
449 }
450 
451 /** Removes the topmost Operation from the OperationStack, calls Operation::undo() on it and deletes it. */
pop()452 void OperationStack::pop()
453 {
454     Operation* o = operations().takeLast();
455     o->undo();
456     delete o;
457     Q_EMIT operationsChanged();
458 }
459 
460 /** Check whether previous operations involve given partition.
461 
462     @param p Pointer to the Partition. Must not be nullptr.
463 */
contains(const Partition * p) const464 bool OperationStack::contains(const Partition* p) const
465 {
466     Q_ASSERT(p);
467 
468     for (const auto &o : operations()) {
469         if (o->targets(*p))
470             return true;
471 
472         CopyOperation* copyOp = dynamic_cast<CopyOperation*>(o);
473         if (copyOp) {
474             const Partition* source = &copyOp->sourcePartition();
475             if (source == p)
476                 return true;
477         }
478     }
479 
480     return false;
481 }
482 
483 /** Removes all Operations from the OperationStack, calling Operation::undo() on them and deleting them. */
clearOperations()484 void OperationStack::clearOperations()
485 {
486     while (!operations().isEmpty()) {
487         Operation* o = operations().takeLast();
488         if (o->status() == Operation::StatusPending)
489             o->undo();
490         delete o;
491     }
492 
493     Q_EMIT operationsChanged();
494 }
495 
496 /** Clears the list of Devices. */
clearDevices()497 void OperationStack::clearDevices()
498 {
499     QWriteLocker lockDevices(&lock());
500 
501     qDeleteAll(previewDevices());
502     previewDevices().clear();
503     Q_EMIT devicesChanged();
504 }
505 
506 /** Finds a Device a Partition is on.
507     @param p pointer to the Partition to find a Device for
508     @return the Device or nullptr if none could be found
509 */
findDeviceForPartition(const Partition * p)510 Device* OperationStack::findDeviceForPartition(const Partition* p)
511 {
512     QReadLocker lockDevices(&lock());
513 
514     const auto devices = previewDevices();
515     for (Device *d : devices) {
516         if (d->partitionTable() == nullptr)
517             continue;
518 
519         const auto partitions = d->partitionTable()->children();
520         for (const auto *part : partitions) {
521             if (part == p)
522                 return d;
523 
524             for (const auto &child : part->children())
525                 if (child == p)
526                     return d;
527         }
528     }
529 
530     return nullptr;
531 }
532 
533 /** Adds a Device to the OperationStack
534     @param d pointer to the Device to add. Must not be nullptr.
535 */
addDevice(Device * d)536 void OperationStack::addDevice(Device* d)
537 {
538     Q_ASSERT(d);
539 
540     QWriteLocker lockDevices(&lock());
541 
542     previewDevices().append(d);
543     Q_EMIT devicesChanged();
544 }
545 
deviceLessThan(const Device * d1,const Device * d2)546 static bool deviceLessThan(const Device* d1, const Device* d2)
547 {
548     // Display alphabetically sorted disk devices above LVM VGs
549     if (d1->type() == Device::Type::LVM_Device && d2->type() == Device::Type::Disk_Device )
550         return false;
551 
552     return d1->deviceNode() <= d2->deviceNode();
553 }
554 
sortDevices()555 void OperationStack::sortDevices()
556 {
557     QWriteLocker lockDevices(&lock());
558 
559     std::sort(previewDevices().begin(), previewDevices().end(), deviceLessThan);
560 
561     Q_EMIT devicesChanged();
562 }
563