Adding disk optimization. Pascal only.

This commit is contained in:
a2geek committed 2026-08-28 11:45:49 -05:00
1 parent 811128623a
commit 8b8749e57c
9 files changed
+316 -2

No files matched your search

@@ -57,6 +57,7 @@ import java.util.logging.Logger;
ListCommand.class,
LockCommand.class,
MkdirCommand.class,
OptimizeCommand.class,
ProofCommand.class,
ReadCommand.class,
RenameFileCommand.class,
@@ -0,0 +1,55 @@
/*
* AppleCommander - An Apple ][ image utility.
* Copyright (C) 2026 by Robert Greene and others
* robgreene at users.sourceforge.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package io.github.applecommander.acx.command;
import com.webcodepro.applecommander.storage.FormattedDisk;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import org.applecommander.os.DiskOptimizer;
import picocli.CommandLine.Command;
import java.util.Optional;
@Command(name = "optimize", aliases = { "krunch" }, description = "Optimize disk image.")
public class OptimizeCommand extends ReadWriteDiskCommandOptions {
@Override
public int handleCommand() throws Exception {
int completed = 0;
int skipped = 0;
for (FormattedDisk disk : selectedDisks()) {
Optional<DiskOptimizer> opt = disk.get(DiskOptimizer.class);
if (opt.isEmpty()) {
System.out.printf("Optimization not supported for disk %s of type %s.\n", disk.getFilename(), disk.getFormat());
skipped++;
continue;
}
DiskOptimizer optimizer = opt.get();
if (!optimizer.canOptimizeDisk()) {
System.out.printf("Disk %s does not need optimization.\n", disk.getFilename());
skipped++;
continue;
}
optimizer.optimizeDisk();
completed++;
System.out.printf("Disk %s has been optimized.\n", disk.getFilename());
}
System.out.printf("%d disks were optimized, %d disks were skipped.\n", completed, skipped);
return 0;
}
}
@@ -0,0 +1,79 @@
/*
* AppleCommander - An Apple ][ image utility.
* Copyright (C) 2026 by Robert Greene and others
* robgreene at users.sourceforge.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.webcodepro.applecommander.storage.os.pascal;
import com.webcodepro.applecommander.storage.FileEntry;
import org.applecommander.os.DiskOptimizer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* A simple Pascal disk optimizer. This should emulate a "krunch" to the front of the disk.
*/
public class PascalDiskOptimizer implements DiskOptimizer {
private PascalFormatDisk disk;
public PascalDiskOptimizer(PascalFormatDisk disk) {
this.disk = disk;
}
@Override
public boolean canOptimizeDisk() {
int nextBlock = 6;
for (PascalFileEntry fileEntry : getSortedFiles()) {
if (fileEntry.getFirstBlock() != nextBlock) {
return true;
}
nextBlock = fileEntry.getLastBlock();
}
return false;
}
@Override
public void optimizeDisk() {
int nextBlock = 6;
for (PascalFileEntry fileEntry : getSortedFiles()) {
if (fileEntry.getFirstBlock() != nextBlock) {
int offset = fileEntry.getFirstBlock() - nextBlock;
// We move both pointers where we think they should be. And then save the file to move the data.
byte[] fileData = fileEntry.getFileData();
fileEntry.setFirstBlock(fileEntry.getFirstBlock() - offset);
fileEntry.setLastBlock(fileEntry.getLastBlock() - offset);
try {
fileEntry.setFileData(fileData);
} catch (Exception e) {
throw new RuntimeException("Unexpected error", e);
}
}
nextBlock = fileEntry.getLastBlock();
}
}
public List<PascalFileEntry> getSortedFiles() {
List<PascalFileEntry> sortedFiles = new ArrayList<>();
for (FileEntry fileEntry : disk.getFiles()) {
sortedFiles.add((PascalFileEntry) fileEntry);
}
sortedFiles.sort(Comparator.comparingInt(PascalFileEntry::getFirstBlock));
return sortedFiles;
}
}
@@ -119,6 +119,7 @@ public class PascalFormatDisk extends FormattedDisk {
}
private final BlockDevice device;
private final PascalDiskOptimizer optimizer;
/**
* Constructor for PascalFormatDisk.
@@ -126,6 +127,7 @@ public class PascalFormatDisk extends FormattedDisk {
public PascalFormatDisk(String filename, BlockDevice device) {
super(filename, device.get(Source.class).orElseThrow());
this.device = device;
this.optimizer = new PascalDiskOptimizer(this);
}
/**
@@ -147,7 +149,7 @@ public class PascalFormatDisk extends FormattedDisk {
@Override
public <T> Optional<T> get(Class<T> iface) {
return Container.get(iface, device);
return Container.get(iface, device, optimizer);
}
/**
@@ -0,0 +1,38 @@
/*
* AppleCommander - An Apple ][ image utility.
* Copyright (C) 2026 by Robert Greene and others
* robgreene at users.sourceforge.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.applecommander.os;
/**
* An optional disk optimizer interface.
*
* @see org.applecommander.util.Container#get(Class)
*/
public interface DiskOptimizer {
/**
* Indicates if this disk can be optimized.
* For instance, a Pascal disk can be "krunched" to pack the files together but it may not need it.
*/
boolean canOptimizeDisk();
/**
* Perform disk optimization.
*/
void optimizeDisk();
}
@@ -0,0 +1,91 @@
/*
* AppleCommander - An Apple ][ image utility.
* Copyright (C) 2002-2026 by Robert Greene and others
* robgreene at users.sourceforge.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.webcodepro.applecommander.storage.os.pascal;
import com.webcodepro.applecommander.storage.DiskConstants;
import com.webcodepro.applecommander.storage.DiskFullException;
import com.webcodepro.applecommander.storage.FileEntry;
import org.applecommander.device.BlockDevice;
import org.applecommander.device.ProdosOrderedBlockDevice;
import org.applecommander.os.DiskOptimizer;
import org.applecommander.source.DataBufferSource;
import org.applecommander.source.Source;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
public class PascalDiskOptimizerTest {
@Test
public void testKrunch() throws DiskFullException {
Source source = DataBufferSource.create(DiskConstants.APPLE_140KB_DISK, "new-disk").get();
BlockDevice blockDevice = new ProdosOrderedBlockDevice(source, BlockDevice.STANDARD_BLOCK_SIZE);
PascalFormatDisk[] disks = PascalFormatDisk.create("deleteme.po", "TEST", blockDevice);
PascalFormatDisk disk = disks[0];
// Strategy:
// 1. Create a set of files. At least one single block file.
// 2. Delete the first file (opening up a gap).
// 3. "Krunch"
// 4. Check expected block numbers.
PascalFileEntry file0 = createFileData(disk, "file0", 2000); // 4 blocks (gets deleted)
PascalFileEntry file1 = createFileData(disk, "file1", 2048); // 4 blocks
PascalFileEntry file2 = createFileData(disk, "file2", 4000); // 8 blocks
PascalFileEntry file3 = createFileData(disk, "file3", 500); // 1 block
PascalFileEntry file4 = createFileData(disk, "file4", 2000); // 4 blocks
// Need to capture the random data BEFORE optimization occurs
final byte[][] expectedFileData = { file1.getFileData(), file2.getFileData(), file3.getFileData(), file4.getFileData() };
DiskOptimizer optimizer = disk.get(DiskOptimizer.class).orElseThrow();
assertFalse(optimizer.canOptimizeDisk());
file0.delete();
assertTrue(optimizer.canOptimizeDisk());
optimizer.optimizeDisk();
// Some of the file data is cached, so we do a fresh pull from the image
final int[] expectedFirstBlock = { 6, 10, 18, 19 };
final int[] expectedLastBlock = { 10, 18, 19, 23 };
List<FileEntry> fileEntries = disk.getFiles();
assertEquals(4, fileEntries.size());
for (int i=0; i<fileEntries.size(); i++) {
PascalFileEntry fileEntry = (PascalFileEntry) fileEntries.get(i);
assertEquals(expectedFirstBlock[i], fileEntry.getFirstBlock());
assertEquals(expectedLastBlock[i], fileEntry.getLastBlock());
assertArrayEquals(expectedFileData[i], fileEntry.getFileData());
}
}
public PascalFileEntry createFileData(PascalFormatDisk disk, String filename, int size) throws DiskFullException {
byte[] fileData = new byte[size];
new Random().nextBytes(fileData);
PascalFileEntry fileEntry = disk.createFile();
fileEntry.setFilename(filename);
fileEntry.setFiletype("data");
fileEntry.setFileData(fileData);
return fileEntry;
}
}
@@ -49,6 +49,7 @@ import org.applecommander.device.Device;
import org.applecommander.hint.Hint;
import org.applecommander.image.NibbleImage;
import org.applecommander.os.DiskCheck;
import org.applecommander.os.DiskOptimizer;
import org.applecommander.source.DataBufferSource;
import org.applecommander.source.Source;
import org.applecommander.source.Sources;
@@ -112,6 +113,7 @@ public class DiskExplorerTab {
private ToolItem saveAsToolItem;
private ToolItem changeOrderToolItem;
private ToolItem checkDiskToolItem;
private ToolItem optimizeDiskToolItem;
private Menu changeImageOrderMenu;
private final UserPreferences userPreferences = UserPreferences.getInstance();
@@ -1342,7 +1344,25 @@ public class DiskExplorerTab {
}
}
});
optimizeDiskToolItem = new ToolItem(toolBar, SWT.PUSH);
optimizeDiskToolItem.setImage(imageManager.get(ImageManager.ICON_DISK_OPTIMIZE));
optimizeDiskToolItem.setText("Optimize");
optimizeDiskToolItem.setToolTipText("Optimize disk.");
optimizeDiskToolItem.setEnabled(false);
for (FormattedDisk disk : disks) {
if (disk.get(DiskOptimizer.class).isPresent()) optimizeDiskToolItem.setEnabled(true);
}
optimizeDiskToolItem.addSelectionListener(new SelectionAdapter () {
public void widgetSelected(SelectionEvent e) {
try {
optimizeDisk();
} catch (Exception ex) {
showError(ex);
}
}
});
new ToolItem(toolBar, SWT.SEPARATOR);
saveToolItem = new ToolItem(toolBar, SWT.PUSH);
@@ -2198,6 +2218,32 @@ public class DiskExplorerTab {
dialog.open();
}
protected void optimizeDisk() {
int completed = 0;
int didntNeed = 0;
int skipped = 0;
for (FormattedDisk disk : disks) {
Optional<DiskOptimizer> opt = disk.get(DiskOptimizer.class);
if (opt.isEmpty()) {
skipped++;
continue;
}
DiskOptimizer optimizer = opt.get();
if (!optimizer.canOptimizeDisk()) {
didntNeed++;
continue;
}
optimizer.optimizeDisk();
completed++;
}
MessageBox box = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.CLOSE);
box.setText("Optimization results");
box.setMessage(String.format("%d disks were optimized, %d disks didn't need optimization, and %d disks were skipped.",
completed, didntNeed, skipped));
box.open();
}
/**
* Construct the popup menu for the export button on the toolbar.
*/
@@ -72,6 +72,7 @@ public class ImageManager {
public static final String ICON_HEALTH_CHECK = "health-check.png";
public static final String ICON_PLUS = "plus.png";
public static final String ICON_MINUS = "minus.png";
public static final String ICON_DISK_OPTIMIZE = "disk-optimize.png";
public static final String LOGO_EXPORT_WIZARD = "ExportWizardLogo.jpg";
public static final String LOGO_APPLECOMMANDER = "AppleCommanderLogo.jpg";
@@ -98,6 +99,7 @@ public class ImageManager {
ICON_CHANGE_IMAGE_ORDER, ICON_SHAPE_TABLE,
ICON_SETTINGS, ICON_HEALTH_CHECK,
ICON_PLUS, ICON_MINUS,
ICON_DISK_OPTIMIZE,
// Logos:
LOGO_EXPORT_WIZARD, LOGO_APPLECOMMANDER,
LOGO_DISK_IMAGE_WIZARD, LOGO_IMPORT_WIZARD,
Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B