From 8b8749e57cc4decdaded083a0ffba7d01e8666de Mon Sep 17 00:00:00 2001 From: a2geek Date: Fri, 28 Aug 2026 11:45:49 -0500 Subject: [PATCH] Adding disk optimization. Pascal only. --- .../io/github/applecommander/acx/Main.java | 1 + .../acx/command/OptimizeCommand.java | 55 +++++++++++ .../os/pascal/PascalDiskOptimizer.java | 79 +++++++++++++++ .../storage/os/pascal/PascalFormatDisk.java | 4 +- .../org/applecommander/os/DiskOptimizer.java | 38 ++++++++ .../os/pascal/PascalDiskOptimizerTest.java | 91 ++++++++++++++++++ .../ui/swt/DiskExplorerTab.java | 48 ++++++++- .../ui/swt/util/ImageManager.java | 2 + .../ui/images/disk-optimize.png | Bin 0 -> 550 bytes 9 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 app/cli-acx/src/main/java/io/github/applecommander/acx/command/OptimizeCommand.java create mode 100644 lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizer.java create mode 100644 lib/ac-api/src/main/java/org/applecommander/os/DiskOptimizer.java create mode 100644 lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizerTest.java create mode 100644 lib/ac-swt-common/src/main/resources/com/webcodepro/applecommander/ui/images/disk-optimize.png diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java index 8abe7e21..cecdef54 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/Main.java @@ -57,6 +57,7 @@ import java.util.logging.Logger; ListCommand.class, LockCommand.class, MkdirCommand.class, + OptimizeCommand.class, ProofCommand.class, ReadCommand.class, RenameFileCommand.class, diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/OptimizeCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/OptimizeCommand.java new file mode 100644 index 00000000..ee27c725 --- /dev/null +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/OptimizeCommand.java @@ -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 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; + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizer.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizer.java new file mode 100644 index 00000000..334075af --- /dev/null +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizer.java @@ -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 getSortedFiles() { + List sortedFiles = new ArrayList<>(); + for (FileEntry fileEntry : disk.getFiles()) { + sortedFiles.add((PascalFileEntry) fileEntry); + } + sortedFiles.sort(Comparator.comparingInt(PascalFileEntry::getFirstBlock)); + return sortedFiles; + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalFormatDisk.java index 03573bb0..1bb47e18 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/pascal/PascalFormatDisk.java @@ -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 Optional get(Class iface) { - return Container.get(iface, device); + return Container.get(iface, device, optimizer); } /** diff --git a/lib/ac-api/src/main/java/org/applecommander/os/DiskOptimizer.java b/lib/ac-api/src/main/java/org/applecommander/os/DiskOptimizer.java new file mode 100644 index 00000000..e776e7ec --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/os/DiskOptimizer.java @@ -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(); +} diff --git a/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizerTest.java b/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizerTest.java new file mode 100644 index 00000000..c7b7e0ed --- /dev/null +++ b/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/os/pascal/PascalDiskOptimizerTest.java @@ -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 fileEntries = disk.getFiles(); + assertEquals(4, fileEntries.size()); + for (int i=0; i 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. */ diff --git a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/util/ImageManager.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/util/ImageManager.java index da741241..93003ef3 100644 --- a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/util/ImageManager.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/util/ImageManager.java @@ -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, diff --git a/lib/ac-swt-common/src/main/resources/com/webcodepro/applecommander/ui/images/disk-optimize.png b/lib/ac-swt-common/src/main/resources/com/webcodepro/applecommander/ui/images/disk-optimize.png new file mode 100644 index 0000000000000000000000000000000000000000..4b37e7d34bb12941efee21bb4294470e94d87638 GIT binary patch literal 550 zcmV+>0@?kEP)En=)9rO0cl;M--KaRu+QV zSVmSM3#KqOR)VEqB)dsqXO7HdHW_!L!LNFA?zx9I@4YM9I9guriyWRcoCFS~^1RLK zt|0efwzBUwS5K6#D{FKj<@}4|_+_Ab*h6k2#FEnrQH(db8)e3KJ`P6op@a zF2N64r4sd%_ux0F_Il;!J9z2=k4SC8#_GS3v_`a8vpj-6Get5+H9;^pLqRw~HOnfsZU6uP07*qoM6N<$f-l1K{Qv*} literal 0 HcmV?d00001