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 8add951..fce9895 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 @@ -24,6 +24,7 @@ import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import io.github.applecommander.acx.command.CompareCommand; import io.github.applecommander.acx.command.ConvertCommand; import io.github.applecommander.acx.command.CopyFileCommand; import io.github.applecommander.acx.command.CreateDiskCommand; @@ -53,6 +54,7 @@ import picocli.CommandLine.Option; optionListHeading = "%nOptions:%n", description = "'acx' experimental utility", subcommands = { + CompareCommand.class, ConvertCommand.class, CopyFileCommand.class, CreateDiskCommand.class, diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CompareCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CompareCommand.java new file mode 100644 index 0000000..fa2e26d --- /dev/null +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CompareCommand.java @@ -0,0 +1,110 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2019-2022 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 java.util.Optional; +import java.util.function.Consumer; + +import com.webcodepro.applecommander.storage.Disk; +import com.webcodepro.applecommander.storage.compare.ComparisonResult; +import com.webcodepro.applecommander.storage.compare.DiskDiff; + +import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions; +import io.github.applecommander.acx.converter.DiskConverter; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "compare", description = "Compare two disk images.") +public class CompareCommand extends ReadOnlyDiskImageCommandOptions { + @Parameters(arity = "1", converter = DiskConverter.class, description = "Second image to compare to.") + private Disk disk2; + + @ArgGroup(heading = "%nComparison Strategy Selection:%n") + private StrategySelection strategySelection = new StrategySelection(); + + @Option(names = { "-l", "--limit" }, description = "Set limit to messages displayed.") + private Optional limit = Optional.empty(); + + @Override + public int handleCommand() throws Exception { + DiskDiff.Builder builder = DiskDiff.create(disk, disk2); + strategySelection.strategy.accept(builder); + ComparisonResult result = builder.compare(); + + if (result.getDifferenceCount() == 0) { + System.out.println("The disks match."); + } + else { + System.out.println("The disks do not match."); + limit.map(result::getLimitedMessages) + .orElseGet(result::getAllMessages) + .forEach(System.out::println); + if (result.getDifferenceCount() > limit.orElse(Integer.MAX_VALUE)) { + System.out.printf("There are %d more messages.\n", result.getDifferenceCount() - limit.get()); + } + return 1; + } + + return 0; + } + + public static class StrategySelection { + private Consumer strategy = this::nativeGeometry; + + @Option(names = "--native", description = "Compare by native geometry.") + private void selectNativeGeometry(boolean flag) { + strategy = this::nativeGeometry; + } + @Option(names = "--block", description = "Compare by block geometry.") + private void selectBlockGeometry(boolean flag) { + strategy = this::blockGeometry; + } + @Option(names = { "--track-sector", "--ts" }, description = "Compare by track/sector geometry.") + private void selectTrackSectorGeometry(boolean flag) { + strategy = this::trackSectorGeometry; + } + @Option(names = { "--filename" }, description = "Compare by filename.") + private void selectByFilename(boolean flag) { + strategy = this::filename; + } + @Option(names = { "--content" }, description = "Compare by file content.") + private void selectByFileContent(boolean flag) { + strategy = this::fileContent; + } + + private void nativeGeometry(DiskDiff.Builder builder) { + builder.selectCompareByNativeGeometry(); + } + private void blockGeometry(DiskDiff.Builder builder) { + builder.selectCompareByBlockGeometry(); + } + private void trackSectorGeometry(DiskDiff.Builder builder) { + builder.selectCompareByTrackSectorGeometry(); + } + private void filename(DiskDiff.Builder builder) { + builder.selectCompareByFileName(); + } + private void fileContent(DiskDiff.Builder builder) { + builder.selectCompareByFileContent(); + } + } +} diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ImportCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ImportCommand.java index a7fda41..75efa07 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ImportCommand.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ImportCommand.java @@ -41,15 +41,15 @@ import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk; import com.webcodepro.applecommander.util.AppleUtil; import com.webcodepro.applecommander.util.StreamUtil; import com.webcodepro.applecommander.util.TranslatorStream; +import com.webcodepro.applecommander.util.readerwriter.FileEntryReader; +import com.webcodepro.applecommander.util.readerwriter.OverrideFileEntryReader; import com.webcodepro.shrinkit.HeaderBlock; import com.webcodepro.shrinkit.NuFileArchive; import com.webcodepro.shrinkit.ThreadRecord; import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions; import io.github.applecommander.acx.converter.IntegerTypeConverter; -import io.github.applecommander.acx.fileutil.FileEntryReader; import io.github.applecommander.acx.fileutil.FileUtils; -import io.github.applecommander.acx.fileutil.OverrideFileEntryReader; import io.github.applecommander.applesingle.AppleSingle; import io.github.applecommander.applesingle.FileDatesInfo; import io.github.applecommander.applesingle.ProdosFileInfo; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileUtils.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileUtils.java index f9979fb..9f21e5f 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileUtils.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileUtils.java @@ -25,6 +25,8 @@ import java.util.logging.Logger; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskException; import com.webcodepro.applecommander.storage.FileEntry; +import com.webcodepro.applecommander.util.readerwriter.FileEntryReader; +import com.webcodepro.applecommander.util.readerwriter.FileEntryWriter; import io.github.applecommander.acx.command.CopyFileCommand; diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskGeometry.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskGeometry.java new file mode 100644 index 0000000..81bb424 --- /dev/null +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskGeometry.java @@ -0,0 +1,39 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2021-2022 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; + +/** + * Indicates the broad disk geometry - track/sector or block. + * Note that BLOCK is meant to include only ProDOS/Pascal 512-byte + * blocks and not the RDOS 256 "blocks" (RDOS should remain under + * the track/sector geometry.) + */ +public enum DiskGeometry { + TRACK_SECTOR(256, "Track/Sector"), + BLOCK(512, "Block"); + + public int size; + public String text; + + private DiskGeometry(int size, String text) { + this.size = size; + this.text = text; + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/FormattedDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/FormattedDisk.java index 52dd48e..4c72208 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/FormattedDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/FormattedDisk.java @@ -399,4 +399,9 @@ public abstract class FormattedDisk extends Disk implements DirectoryEntry { * Typically, the FileEntry.setFileData method should be used. */ public abstract void setFileData(FileEntry fileEntry, byte[] fileData) throws DiskFullException; + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public abstract DiskGeometry getDiskGeometry(); } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/ComparisonResult.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/ComparisonResult.java new file mode 100644 index 0000000..5ba3d28 --- /dev/null +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/ComparisonResult.java @@ -0,0 +1,63 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2021-2022 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.compare; + +import java.util.ArrayList; +import java.util.List; + +public class ComparisonResult { + private List errors = new ArrayList<>(); + private List warnings = new ArrayList<>(); + + public boolean hasErrors() { + return !errors.isEmpty(); + } + public int getDifferenceCount() { + return errors.size() + warnings.size(); + } + + public List getAllMessages() { + List messages = new ArrayList<>(); + messages.addAll(errors); + messages.addAll(warnings); + return messages; + } + public List getLimitedMessages(int limit) { + List messages = getAllMessages(); + return messages.subList(0, Math.min(messages.size(), limit)); + } + + public void addError(Exception ex) { + errors.add(ex.getMessage()); + } + public void addError(String fmt, Object... args) { + errors.add(String.format(fmt, args)); + } + public void addWarning(String fmt, Object... args) { + warnings.add(String.format(fmt, args)); + } + + public List getErrors() { + return errors; + } + public List getWarnings() { + return warnings; + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/DiskDiff.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/DiskDiff.java new file mode 100644 index 0000000..25d523f --- /dev/null +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/compare/DiskDiff.java @@ -0,0 +1,393 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2021-2022 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.compare; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import com.webcodepro.applecommander.storage.Disk; +import com.webcodepro.applecommander.storage.DiskException; +import com.webcodepro.applecommander.storage.DiskGeometry; +import com.webcodepro.applecommander.storage.DiskUnrecognizedException; +import com.webcodepro.applecommander.storage.FormattedDisk; +import com.webcodepro.applecommander.storage.physical.ImageOrder; +import com.webcodepro.applecommander.util.Range; +import com.webcodepro.applecommander.util.filestreamer.FileStreamer; +import com.webcodepro.applecommander.util.filestreamer.FileTuple; +import com.webcodepro.applecommander.util.filestreamer.TypeOfFile; +import com.webcodepro.applecommander.util.readerwriter.FileEntryReader; + +/** + * Perform a disk comparison based on selected strategy. + */ +public class DiskDiff { + public static ComparisonResult compare(Disk diskA, Disk diskB) { + return new DiskDiff(diskA, diskB).compare(); + } + public static Builder create(Disk diskA, Disk diskB) { + return new Builder(diskA, diskB); + } + + private Disk diskA; + private Disk diskB; + private ComparisonResult results = new ComparisonResult(); + + private BiConsumer diskComparisonStrategy = this::compareByNativeGeometry; + + private DiskDiff(Disk diskA, Disk diskB) { + Objects.requireNonNull(diskA); + Objects.requireNonNull(diskB); + this.diskA = diskA; + this.diskB = diskB; + } + + public ComparisonResult compare() { + FormattedDisk[] formattedDisksA = null; + try { + formattedDisksA = diskA.getFormattedDisks(); + } catch (DiskUnrecognizedException e) { + results.addError(e); + } + FormattedDisk[] formattedDisksB = null; + try { + formattedDisksB = diskB.getFormattedDisks(); + } catch (DiskUnrecognizedException e) { + results.addError(e); + } + + if (!results.hasErrors()) { + compareAll(formattedDisksA, formattedDisksB); + } + return results; + } + + public void compareAll(FormattedDisk[] formattedDisksA, FormattedDisk[] formattedDisksB) { + Objects.requireNonNull(formattedDisksA); + Objects.requireNonNull(formattedDisksB); + + if (formattedDisksA.length != formattedDisksB.length) { + results.addWarning("Cannot compare all disks; %s has %d while %s has %d.", + diskA.getFilename(), formattedDisksA.length, + diskB.getFilename(), formattedDisksB.length); + } + + int min = Math.min(formattedDisksA.length, formattedDisksB.length); + for (int i=0; i %d)", + orderA.getBlocksOnDevice(), orderB.getBlocksOnDevice()); + return; + } + + List unequalBlocks = new ArrayList<>(); + for (int block=0; block %d)", + orderA.getSectorsPerDisk(), orderB.getSectorsPerDisk()); + return; + } + + for (int track=0; track unequalSectors = new ArrayList<>(); + for (int sector=0; sector> filesA = FileStreamer.forDisk(formattedDiskA) + .includeTypeOfFile(TypeOfFile.FILE) + .recursive(true) + .stream() + .collect(Collectors.groupingBy(FileTuple::fullPath)); + Map> filesB = FileStreamer.forDisk(formattedDiskB) + .includeTypeOfFile(TypeOfFile.FILE) + .recursive(true) + .stream() + .collect(Collectors.groupingBy(FileTuple::fullPath)); + + Set pathsOnlyA = new HashSet<>(filesA.keySet()); + pathsOnlyA.removeAll(filesB.keySet()); + if (!pathsOnlyA.isEmpty()) { + results.addError("Files only in %s: %s", formattedDiskA.getFilename(), String.join(", ", pathsOnlyA)); + } + + Set pathsOnlyB = new HashSet<>(filesB.keySet()); + pathsOnlyB.removeAll(filesA.keySet()); + if (!pathsOnlyB.isEmpty()) { + results.addError("Files only in %s: %s", formattedDiskB.getFilename(), String.join(", ", pathsOnlyB)); + } + + Set pathsInAB = new HashSet<>(filesA.keySet()); + pathsInAB.retainAll(filesB.keySet()); + for (String path : pathsInAB) { + List tuplesA = filesA.get(path); + List tuplesB = filesB.get(path); + + // Since this is by name, we expect a single file; report oddities + FileTuple tupleA = tuplesA.get(0); + if (tuplesA.size() > 1) { + results.addWarning("Path %s on disk %s has %d entries.", path, formattedDiskA.getFilename(), tuplesA.size()); + } + FileTuple tupleB = tuplesB.get(0); + if (tuplesB.size() > 1) { + results.addWarning("Path %s on disk %s has %d entries.", path, formattedDiskB.getFilename(), tuplesB.size()); + } + + // Do our own custom compare so we can capture a description of differences: + FileEntryReader readerA = FileEntryReader.get(tupleA.fileEntry); + FileEntryReader readerB = FileEntryReader.get(tupleB.fileEntry); + List differences = compare(readerA, readerB); + if (!differences.isEmpty()) { + results.addWarning("Path %s differ: %s", path, String.join(", ", differences)); + } + } + } catch (DiskException ex) { + results.addError(ex); + } + } + + /** Compare by file content. Accounts for content differences that are "only" in disk A or "only" in disk B. */ + public void compareByFileContent(FormattedDisk formattedDiskA, FormattedDisk formattedDiskB) { + try { + Map> contentA = FileStreamer.forDisk(formattedDiskA) + .includeTypeOfFile(TypeOfFile.FILE) + .recursive(true) + .stream() + .collect(Collectors.groupingBy(this::contentHash)); + Map> contentB = FileStreamer.forDisk(formattedDiskB) + .includeTypeOfFile(TypeOfFile.FILE) + .recursive(true) + .stream() + .collect(Collectors.groupingBy(this::contentHash)); + + Set contentOnlyA = new HashSet<>(contentA.keySet()); + contentOnlyA.removeAll(contentB.keySet()); + if (!contentOnlyA.isEmpty()) { + Set pathNamesA = contentOnlyA.stream() + .map(contentA::get) + .flatMap(List::stream) + .map(FileTuple::fullPath) + .collect(Collectors.toSet()); + results.addError("Content that only exists in %s: %s", + formattedDiskA.getFilename(), String.join(", ", pathNamesA)); + } + + Set contentOnlyB = new HashSet<>(contentB.keySet()); + contentOnlyB.removeAll(contentA.keySet()); + if (!contentOnlyB.isEmpty()) { + Set pathNamesB = contentOnlyB.stream() + .map(contentB::get) + .flatMap(List::stream) + .map(FileTuple::fullPath) + .collect(Collectors.toSet()); + results.addError("Content that only exists in %s: %s", + formattedDiskB.getFilename(), String.join(", ", pathNamesB)); + } + + Set contentInAB = new HashSet<>(contentA.keySet()); + contentInAB.retainAll(contentB.keySet()); + for (String content : contentInAB) { + List tuplesA = contentA.get(content); + List tuplesB = contentB.get(content); + + // This is by content, but uncertain how to report multiple per disk, so pick first one + FileTuple tupleA = tuplesA.get(0); + if (tuplesA.size() > 1) { + results.addWarning("Hash %s on disk %s has %d entries.", content, + formattedDiskA.getFilename(), tuplesA.size()); + } + FileTuple tupleB = tuplesB.get(0); + if (tuplesB.size() > 1) { + results.addWarning("Hash %s on disk %s has %d entries.", content, + formattedDiskB.getFilename(), tuplesB.size()); + } + + // Do our own custom compare so we can capture a description of differences: + FileEntryReader readerA = FileEntryReader.get(tupleA.fileEntry); + FileEntryReader readerB = FileEntryReader.get(tupleB.fileEntry); + List differences = compare(readerA, readerB); + if (!differences.isEmpty()) { + results.addWarning("Files %s and %s share same content but file attributes differ: %s", + tupleA.fullPath(), tupleB.fullPath(), String.join(", ", differences)); + } + } + } catch (DiskException ex) { + results.addError(ex); + } + } + private String contentHash(FileTuple tuple) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] digest = messageDigest.digest(tuple.fileEntry.getFileData()); + return String.format("%032X", new BigInteger(1, digest)); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + + private List compare(FileEntryReader readerA, FileEntryReader readerB) { + List differences = new ArrayList<>(); + if (!readerA.getFilename().equals(readerB.getFilename())) { + differences.add("filename"); + } + if (!readerA.getProdosFiletype().equals(readerB.getProdosFiletype())) { + differences.add("filetype"); + } + if (!readerA.isLocked().equals(readerB.isLocked())) { + differences.add("locked"); + } + if (!Arrays.equals(readerA.getFileData().orElse(null), readerB.getFileData().orElse(null))) { + differences.add("file data"); + } + if (!Arrays.equals(readerA.getResourceData().orElse(null), readerB.getResourceData().orElse(null))) { + differences.add("resource fork"); + } + if (!readerA.getBinaryAddress().equals(readerB.getBinaryAddress())) { + differences.add("address"); + } + if (!readerA.getBinaryLength().equals(readerB.getBinaryLength())) { + differences.add("length"); + } + if (!readerA.getAuxiliaryType().equals(readerB.getAuxiliaryType())) { + differences.add("aux. type"); + } + if (!readerA.getCreationDate().equals(readerB.getCreationDate())) { + differences.add("create date"); + } + if (!readerA.getLastModificationDate().equals(readerB.getLastModificationDate())) { + differences.add("mod. date"); + } + return differences; + } + + public static class Builder { + private DiskDiff diff; + + public Builder(Disk diskA, Disk diskB) { + diff = new DiskDiff(diskA, diskB); + } + /** Compare disks by whatever native geometry the disks have. Fails if geometries do not match. */ + public Builder selectCompareByNativeGeometry() { + diff.diskComparisonStrategy = diff::compareByNativeGeometry; + return this; + } + /** Compare disks by 256-byte DOS sectors. */ + public Builder selectCompareByTrackSectorGeometry() { + diff.diskComparisonStrategy = diff::compareByTrackSectorGeometry; + return this; + } + /** Compare disks by 512-byte ProDOS/Pascal blocks. */ + public Builder selectCompareByBlockGeometry() { + diff.diskComparisonStrategy = diff::compareByBlockGeometry; + return this; + } + /** Compare disks by files ensuring that all filenames match. */ + public Builder selectCompareByFileName() { + diff.diskComparisonStrategy = diff::compareByFileName; + return this; + } + /** Compare disks by files based on content; allowing files to have moved or been renamed. */ + public Builder selectCompareByFileContent() { + diff.diskComparisonStrategy = diff::compareByFileContent; + return this; + } + + public ComparisonResult compare() { + return diff.compare(); + } + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/cpm/CpmFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/cpm/CpmFormatDisk.java index 9f51a6a..b3bf511 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/cpm/CpmFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/cpm/CpmFormatDisk.java @@ -27,6 +27,7 @@ import java.util.StringTokenizer; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -557,4 +558,11 @@ public class CpmFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.TRACK_SECTOR; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/dos33/DosFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/dos33/DosFormatDisk.java index ef64f38..82f2dc0 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/dos33/DosFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/dos33/DosFormatDisk.java @@ -28,6 +28,7 @@ import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskException; import com.webcodepro.applecommander.storage.DiskCorruptException; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -773,4 +774,11 @@ public class DosFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.TRACK_SECTOR; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/gutenberg/GutenbergFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/gutenberg/GutenbergFormatDisk.java index bb93807..947681b 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/gutenberg/GutenbergFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/gutenberg/GutenbergFormatDisk.java @@ -24,6 +24,7 @@ import java.util.List; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -699,4 +700,11 @@ public class GutenbergFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.TRACK_SECTOR; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/nakedos/NakedosFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/nakedos/NakedosFormatDisk.java index 488a76f..69ab492 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/nakedos/NakedosFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/nakedos/NakedosFormatDisk.java @@ -24,6 +24,7 @@ import java.util.List; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -537,4 +538,11 @@ public class NakedosFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.TRACK_SECTOR; + } } 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 04660a1..2394a5a 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 @@ -28,6 +28,7 @@ import java.util.List; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -667,4 +668,11 @@ public class PascalFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.BLOCK; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java index 873e7b2..6b89c30 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/prodos/ProdosFormatDisk.java @@ -32,6 +32,7 @@ import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskException; import com.webcodepro.applecommander.storage.DiskCorruptException; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -1450,4 +1451,11 @@ public class ProdosFormatDisk extends FormattedDisk { throw new DiskFullException(textBundle.get("ProdosFormatDisk.UnableToAllocateFileEntry"), this.getFilename()); } } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.BLOCK; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/rdos/RdosFormatDisk.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/rdos/RdosFormatDisk.java index 1c3878d..7c7ef37 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/rdos/RdosFormatDisk.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/os/rdos/RdosFormatDisk.java @@ -25,6 +25,7 @@ import java.util.List; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskFullException; +import com.webcodepro.applecommander.storage.DiskGeometry; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; @@ -518,4 +519,11 @@ public class RdosFormatDisk extends FormattedDisk { public DirectoryEntry createDirectory(String name) throws DiskFullException { throw new UnsupportedOperationException(textBundle.get("DirectoryCreationNotSupported")); //$NON-NLS-1$ } + + /** + * Gives an indication on how this disk's geometry should be handled. + */ + public DiskGeometry getDiskGeometry() { + return DiskGeometry.TRACK_SECTOR; + } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/AppleUtil.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/AppleUtil.java index 6487f88..cd59083 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/AppleUtil.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/AppleUtil.java @@ -22,12 +22,10 @@ package com.webcodepro.applecommander.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.physical.ImageOrder; /** @@ -615,28 +613,6 @@ public class AppleUtil { protected static boolean sameSectorsPerDisk(ImageOrder sourceOrder, ImageOrder targetOrder) { return sourceOrder.getSectorsPerDisk() == targetOrder.getSectorsPerDisk(); } - - /** - * Compare two disks by track and sector. - */ - public static boolean disksEqualByTrackAndSector(FormattedDisk sourceDisk, FormattedDisk targetDisk) { - ImageOrder sourceOrder = sourceDisk.getImageOrder(); - ImageOrder targetOrder = targetDisk.getImageOrder(); - if (!sameSectorsPerDisk(sourceOrder, targetOrder)) { - throw new IllegalArgumentException(textBundle. - get("AppleUtil.CannotCompareDisks")); //$NON-NLS-1$ - } - for (int track = 0; track < sourceOrder.getTracksPerDisk(); track++) { - for (int sector = 0; sector < sourceOrder.getSectorsPerTrack(); sector++) { - byte[] sourceData = sourceOrder.readSector(track, sector); - byte[] targetData = targetOrder.readSector(track, sector); - if (!Arrays.equals(sourceData, targetData)) { - return false; - } - } - } - return true; - } /** * Change ImageOrder from source order to target order by copying block by block. @@ -658,24 +634,4 @@ public class AppleUtil { protected static boolean sameBlocksPerDisk(ImageOrder sourceOrder, ImageOrder targetOrder) { return sourceOrder.getBlocksOnDevice() == targetOrder.getBlocksOnDevice(); } - - /** - * Compare two disks block by block. - */ - public static boolean disksEqualByBlock(FormattedDisk sourceDisk, FormattedDisk targetDisk) { - ImageOrder sourceOrder = sourceDisk.getImageOrder(); - ImageOrder targetOrder = targetDisk.getImageOrder(); - if (!sameBlocksPerDisk(sourceOrder, targetOrder)) { - throw new IllegalArgumentException(textBundle. - get("AppleUtil.CannotCompareDisks")); //$NON-NLS-1$ - } - for (int block = 0; block < sourceOrder.getBlocksOnDevice(); block++) { - byte[] sourceData = sourceOrder.readBlock(block); - byte[] targetData = targetOrder.readBlock(block); - if (!Arrays.equals(sourceData, targetData)) { - return false; - } - } - return true; - } } diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/Range.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/Range.java new file mode 100644 index 0000000..fcb5055 --- /dev/null +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/Range.java @@ -0,0 +1,89 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2002-2022 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.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a range of numbers with helper methods to put them together. + */ +public class Range { + private int first; + private int last; + + public Range(int first, int last) { + if (first < last) { + this.first = first; + this.last = last; + } + else { + this.first = last; + this.last = first; + } + } + + public int getFirst() { + return first; + } + public int getLast() { + return last; + } + public int size() { + return last - first + 1; + } + + @Override + public String toString() { + if (first == last) { + return String.format("%d", first); + } + else { + return String.format("%d-%d", first, last); + } + } + + public static List from(List numbers) { + List ranges = new ArrayList<>(); + Collections.sort(numbers); + + int first = -1; + int last = -1; + for (int number : numbers) { + if (first == -1) { + first = last = number; + } + else if (number == last+1) { + last = number; + } + else { + ranges.add(new Range(first, last)); + first = last = number; + } + } + + if (first != -1) { + ranges.add(new Range(first, last)); + } + + return ranges; + } +} diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileStreamer.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileStreamer.java index 37a1a82..8d19380 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileStreamer.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileStreamer.java @@ -72,6 +72,9 @@ public class FileStreamer { public static FileStreamer forDisk(Disk disk) throws DiskUnrecognizedException { return new FileStreamer(disk); } + public static FileStreamer forFormattedDisks(FormattedDisk... disks) { + return new FileStreamer(disks); + } private FormattedDisk[] formattedDisks = null; @@ -89,7 +92,10 @@ public class FileStreamer { private List pathMatchers = new ArrayList<>(); private FileStreamer(Disk disk) throws DiskUnrecognizedException { - this.formattedDisks = disk.getFormattedDisks(); + this(disk.getFormattedDisks()); + } + private FileStreamer(FormattedDisk... disks) { + this.formattedDisks = disks; } public FileStreamer ignoreErrors(boolean flag) { diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileTuple.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileTuple.java index b87dc8a..9bd9a1f 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileTuple.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/filestreamer/FileTuple.java @@ -29,6 +29,7 @@ import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; public class FileTuple { + public static final String SEPARATOR = "/"; private static final Logger LOG = Logger.getLogger(FileTuple.class.getName()); public final FormattedDisk formattedDisk; public final List paths; @@ -54,6 +55,9 @@ public class FileTuple { public FileTuple of(FileEntry fileEntry) { return new FileTuple(formattedDisk, paths, directoryEntry, fileEntry); } + public String fullPath() { + return String.join(SEPARATOR, String.join(SEPARATOR, paths), fileEntry.getFilename()); + } public static FileTuple of(FormattedDisk disk) { return new FileTuple(disk, new ArrayList(), (DirectoryEntry)disk, null); diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/DosFileEntryReaderWriter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/DosFileEntryReaderWriter.java similarity index 98% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/DosFileEntryReaderWriter.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/DosFileEntryReaderWriter.java index dbb55a7..c57a11f 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/DosFileEntryReaderWriter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/DosFileEntryReaderWriter.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Map; import java.util.Optional; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryReader.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryReader.java similarity index 80% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryReader.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryReader.java index 1eaccf3..bdaa656 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryReader.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryReader.java @@ -17,8 +17,9 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; +import java.util.Arrays; import java.util.Date; import java.util.Optional; @@ -52,6 +53,19 @@ public interface FileEntryReader { // ProdosFileEntry / PascalFileEntry specific public default Optional getLastModificationDate() { return Optional.empty(); } + public default boolean equals(FileEntryReader reader) { + return getFilename().equals(reader.getFilename()) + && getProdosFiletype().equals(reader.getProdosFiletype()) + && isLocked().equals(reader.isLocked()) + && Arrays.equals(getFileData().orElse(null), reader.getFileData().orElse(null)) + && Arrays.equals(getResourceData().orElse(null), reader.getResourceData().orElse(null)) + && getBinaryAddress().equals(reader.getBinaryAddress()) + && getBinaryLength().equals(reader.getBinaryLength()) + && getAuxiliaryType().equals(reader.getAuxiliaryType()) + && getCreationDate().equals(reader.getCreationDate()) + && getLastModificationDate().equals(reader.getLastModificationDate()); + } + public static FileEntryReader get(FileEntry fileEntry) { if (fileEntry instanceof DosFileEntry) { return new DosFileEntryReaderWriter((DosFileEntry)fileEntry); diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryWriter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryWriter.java similarity index 98% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryWriter.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryWriter.java index dc5981f..8114137 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/FileEntryWriter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/FileEntryWriter.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Date; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/NakedosFileEntryReader.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/NakedosFileEntryReader.java similarity index 96% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/NakedosFileEntryReader.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/NakedosFileEntryReader.java index 1af8192..dea8c2d 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/NakedosFileEntryReader.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/NakedosFileEntryReader.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Optional; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/OverrideFileEntryReader.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/OverrideFileEntryReader.java similarity index 99% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/OverrideFileEntryReader.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/OverrideFileEntryReader.java index cec591a..7413d0b 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/OverrideFileEntryReader.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/OverrideFileEntryReader.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Arrays; import java.util.Date; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/PascalFileEntryReaderWriter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/PascalFileEntryReaderWriter.java similarity index 98% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/PascalFileEntryReaderWriter.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/PascalFileEntryReaderWriter.java index 5c57f66..f521a40 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/PascalFileEntryReaderWriter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/PascalFileEntryReaderWriter.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Date; import java.util.Map; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/ProdosFileEntryReaderWriter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/ProdosFileEntryReaderWriter.java similarity index 98% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/ProdosFileEntryReaderWriter.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/ProdosFileEntryReaderWriter.java index 7fa71a9..a55d089 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/ProdosFileEntryReaderWriter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/ProdosFileEntryReaderWriter.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Date; import java.util.Optional; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/RdosFileEntryReader.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/RdosFileEntryReader.java similarity index 97% rename from app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/RdosFileEntryReader.java rename to lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/RdosFileEntryReader.java index 5cdd7ee..15fefb8 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/fileutil/RdosFileEntryReader.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/util/readerwriter/RdosFileEntryReader.java @@ -17,7 +17,7 @@ * 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.fileutil; +package com.webcodepro.applecommander.util.readerwriter; import java.util.Map; import java.util.Optional; diff --git a/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties b/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties index 3b6156f..db48f52 100644 --- a/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties +++ b/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties @@ -289,10 +289,10 @@ CompareDisksStartPane.DiskNLabel=Please select disk image \#{0}: # CompareDisksResultsPane CompareDisksResultsPane.RestartText=If you wish to compare more disks, click back and start again. -CompareDisksResultsPane.UnableToLoadDiskN=Unable to load disk \#{0}: {1}\n -CompareDisksResultsPane.DifferentSizeError=The two disks are of differing formats - unable to compare.\n -CompareDisksResultsPane.DataDiffersMessage=The two disks do not contain the same data.\n -CompareDisksResultsPane.DifferentDataFormatError=The two disks are not the same data format.\n +CompareDisksResultsPane.UnableToLoadDiskN=Unable to load disk \#{0}: {1} +CompareDisksResultsPane.DifferentSizeError=The two disks are of differing formats - unable to compare. +CompareDisksResultsPane.DataDiffersMessage=The two disks do not contain the same data. +CompareDisksResultsPane.DifferentDataFormatError=The two disks are not the same data format. CompareDisksResultsPane.DisksMatch=The disk images match. # GraphicsFilterAdapter diff --git a/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/AppleUtilTest.java b/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/AppleUtilTest.java index 1a36991..8046501 100644 --- a/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/AppleUtilTest.java +++ b/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/AppleUtilTest.java @@ -28,6 +28,8 @@ import org.junit.Test; import com.webcodepro.applecommander.storage.Disk; import com.webcodepro.applecommander.storage.DiskFullException; import com.webcodepro.applecommander.storage.FileEntry; +import com.webcodepro.applecommander.storage.compare.ComparisonResult; +import com.webcodepro.applecommander.storage.compare.DiskDiff; import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk; import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk; import com.webcodepro.applecommander.storage.physical.ByteArrayImageLayout; @@ -92,7 +94,9 @@ public class AppleUtilTest { AppleUtil.changeImageOrderByTrackAndSector(dosDiskDosOrder.getImageOrder(), dosDiskNibbleOrder.getImageOrder()); // Confirm that these disks are identical: - assertTrue(AppleUtil.disksEqualByTrackAndSector(dosDiskDosOrder, dosDiskNibbleOrder)); + ComparisonResult result = DiskDiff.create(dosDiskDosOrder, dosDiskDosOrder) + .selectCompareByTrackSectorGeometry().compare(); + assertEquals("Expected disks to have no differences", 0, result.getDifferenceCount()); } @Test @@ -112,7 +116,9 @@ public class AppleUtilTest { AppleUtil.changeImageOrderByBlock(prodosDiskDosOrder.getImageOrder(), prodosDiskNibbleOrder.getImageOrder()); // Confirm that these disks are identical: - assertTrue(AppleUtil.disksEqualByBlock(prodosDiskDosOrder, prodosDiskNibbleOrder)); + ComparisonResult result = DiskDiff.create(prodosDiskDosOrder, prodosDiskNibbleOrder) + .selectCompareByBlockGeometry().compare(); + assertEquals("Expected disks to have no differences", 0, result.getDifferenceCount()); } @Test diff --git a/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/RangeTest.java b/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/RangeTest.java new file mode 100644 index 0000000..1e7f4bb --- /dev/null +++ b/lib/ac-api/src/test/java/com/webcodepro/applecommander/util/RangeTest.java @@ -0,0 +1,49 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2002-2022 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.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class RangeTest { + @Test + public void testToString() { + assertEquals("1", new Range(1,1).toString()); + assertEquals("1-5", new Range(1,5).toString()); + } + + @Test + public void testFrom() { + assertEquals("[1-3, 5, 7, 9-12]", Range.from(Arrays.asList(1,2,3,5,7,9,10,11,12)).toString()); + } + + @Test + public void testFromUnordered() { + assertEquals("[1-3, 5, 7, 9-12]", Range.from(Arrays.asList(9,10,1,5,2,12,3,11,7)).toString()); + } + + @Test + public void testFromEmpty() { + assertEquals("[]", Range.from(Arrays.asList()).toString()); + } +} diff --git a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksResultsPane.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksResultsPane.java index a63846c..6804068 100644 --- a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksResultsPane.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksResultsPane.java @@ -19,16 +19,19 @@ */ package com.webcodepro.applecommander.ui.swt.wizard.comparedisks; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.swt.SWT; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import com.webcodepro.applecommander.storage.Disk; -import com.webcodepro.applecommander.storage.FormattedDisk; +import com.webcodepro.applecommander.storage.compare.ComparisonResult; +import com.webcodepro.applecommander.storage.compare.DiskDiff; import com.webcodepro.applecommander.ui.UiBundle; import com.webcodepro.applecommander.ui.swt.wizard.WizardPane; -import com.webcodepro.applecommander.util.AppleUtil; import com.webcodepro.applecommander.util.TextBundle; /** @@ -75,6 +78,8 @@ public class CompareDisksResultsPane extends WizardPane { label = new Label(control, SWT.WRAP); label.setText(textBundle.get("CompareDisksResultsPane.RestartText")); //$NON-NLS-1$ + + parent.pack(); } /** * Get the next pane. A null return indicates the end of the wizard. @@ -93,49 +98,47 @@ public class CompareDisksResultsPane extends WizardPane { } protected String compareDisks() { - StringBuffer errorMessages = new StringBuffer(); - FormattedDisk[] disk1 = null; + List errorMessages = new ArrayList<>(); + Disk disk1 = null; try { - disk1 = new Disk(wizard.getDiskname1()).getFormattedDisks(); + disk1 = new Disk(wizard.getDiskname1()); } catch (Throwable t) { - errorMessages.append(textBundle. + errorMessages.add(textBundle. format("CompareDisksResultsPane.UnableToLoadDiskN", //$NON-NLS-1$ 1, t.getLocalizedMessage())); } - FormattedDisk[] disk2 = null; + Disk disk2 = null; try { - disk2 = new Disk(wizard.getDiskname2()).getFormattedDisks(); + disk2 = new Disk(wizard.getDiskname2()); } catch (Throwable t) { - errorMessages.append(textBundle. + errorMessages.add(textBundle. format("CompareDisksResultsPane.UnableToLoadDiskN", //$NON-NLS-1$ 2, t.getLocalizedMessage())); } if (disk1 != null && disk2 != null) { - if (disk1.length != disk2.length) { - errorMessages.append(textBundle.get( - "CompareDisksResultsPane.DifferentSizeError")); //$NON-NLS-1$ - } else { - boolean disk1TSformat = disk1[0].isCpmFormat() || disk1[0].isDosFormat() || disk1[0].isRdosFormat(); - boolean disk2TSformat = disk2[0].isCpmFormat() || disk2[0].isDosFormat() || disk2[0].isRdosFormat(); - if (disk1TSformat && disk2TSformat) { - if (!AppleUtil.disksEqualByTrackAndSector(disk1[0], disk2[0])) { - errorMessages.append(textBundle.get( - "CompareDisksResultsPane.DataDiffersMessage")); //$NON-NLS-1$ - } - } else if (!disk1TSformat && !disk2TSformat) { - if (!AppleUtil.disksEqualByBlock(disk1[0], disk2[0])) { - errorMessages.append(textBundle.get( - "CompareDisksResultsPane.DataDiffersMessage")); //$NON-NLS-1$ - } - } else { - errorMessages.append(textBundle.get( - "CompareDisksResultsPane.DifferentDataFormatError")); //$NON-NLS-1$ - } - } + DiskDiff.Builder builder = DiskDiff.create(disk1, disk2); + switch (wizard.getComparisonStrategy()) { + case 0: + builder.selectCompareByNativeGeometry(); + break; + case 1: + builder.selectCompareByTrackSectorGeometry(); + break; + case 2: + builder.selectCompareByBlockGeometry(); + break; + case 3: + builder.selectCompareByFileName(); + break; + default: + throw new RuntimeException("missing a comparison strategy"); + } + ComparisonResult result = builder.compare(); + errorMessages.addAll(result.getLimitedMessages(wizard.getMessageLimit())); } - if (errorMessages.length() == 0) { + if (errorMessages.size() == 0) { return textBundle.get("CompareDisksResultsPane.DisksMatch"); //$NON-NLS-1$ } - return errorMessages.toString(); + return String.join("\n", errorMessages); } } diff --git a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksStartPane.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksStartPane.java index 8d8b863..8563c18 100644 --- a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksStartPane.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksStartPane.java @@ -24,10 +24,10 @@ import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; @@ -50,6 +50,8 @@ public class CompareDisksStartPane extends WizardPane { private CompareDisksWizard wizard; private Text diskname1Text; private Text diskname2Text; + private Combo comparisonStrategyCombo; + private Text limitText; /** * Constructor for CompareDisksStartPane. */ @@ -75,6 +77,7 @@ public class CompareDisksStartPane extends WizardPane { layout.marginTop = 5; layout.spacing = 3; control.setLayout(layout); + Label label = new Label(control, SWT.WRAP); label.setText(textBundle.get("CompareDisksStartPane.Description")); //$NON-NLS-1$ @@ -84,7 +87,6 @@ public class CompareDisksStartPane extends WizardPane { diskname1Text = new Text(control, SWT.WRAP | SWT.BORDER); if (wizard.getDiskname1() != null) diskname1Text.setText(wizard.getDiskname1()); diskname1Text.setLayoutData(new RowData(300, -1)); - diskname1Text.setBackground(new Color(control.getDisplay(), 255,255,255)); diskname1Text.setFocus(); diskname1Text.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) { @@ -113,7 +115,6 @@ public class CompareDisksStartPane extends WizardPane { diskname2Text = new Text(control, SWT.WRAP | SWT.BORDER); if (wizard.getDiskname2() != null) diskname2Text.setText(wizard.getDiskname2()); diskname2Text.setLayoutData(new RowData(300, -1)); - diskname2Text.setBackground(new Color(control.getDisplay(), 255,255,255)); diskname2Text.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent event) { Text text = (Text) event.getSource(); @@ -134,6 +135,32 @@ public class CompareDisksStartPane extends WizardPane { } } }); + + label = new Label(control, SWT.WRAP); + label.setText("Select comparison time:"); + + comparisonStrategyCombo = new Combo(control, SWT.BORDER | SWT.READ_ONLY); + comparisonStrategyCombo.setItems("Compare by native geometry", + "Compare by track/sector geometry", + "Compare by block geometry", + "Compare by filename"); + comparisonStrategyCombo.select(getWizard().getComparisonStrategy()); + comparisonStrategyCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + getWizard().setComparisonStrategy(comparisonStrategyCombo.getSelectionIndex()); + } + }); + + label = new Label(control, SWT.WRAP); + label.setText("Set limit on messages displayed:"); + + limitText = new Text(control, SWT.WRAP | SWT.BORDER); + limitText.setText(Integer.toString(wizard.getMessageLimit())); + limitText.setLayoutData(new RowData(200, -1)); + limitText.addModifyListener(this::limitTextModifyListener); + + parent.pack(); } /** * Get the next pane. A null return indicates the end of the wizard. @@ -166,4 +193,12 @@ public class CompareDisksStartPane extends WizardPane { return textBundle.format("CompareDisksStartPane.DiskNLabel", //$NON-NLS-1$ diskNumber); } + + protected void limitTextModifyListener(ModifyEvent event) { + try { + getWizard().setMessageLimit(Integer.parseInt(limitText.getText())); + } catch (NumberFormatException e) { + limitText.setText(Integer.toString(getWizard().getMessageLimit())); + } + } } diff --git a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksWizard.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksWizard.java index a06411d..abdb5c2 100644 --- a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksWizard.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/wizard/comparedisks/CompareDisksWizard.java @@ -34,6 +34,8 @@ import com.webcodepro.applecommander.ui.swt.wizard.WizardPane; public class CompareDisksWizard extends Wizard { private String diskname1; private String diskname2; + private int comparisonStrategy = 0; + private int messageLimit = 10; /** * Constructor for ExportWizard. */ @@ -54,11 +56,22 @@ public class CompareDisksWizard extends Wizard { public String getDiskname2() { return diskname2; } + public int getComparisonStrategy() { + return comparisonStrategy; + } + public int getMessageLimit() { + return messageLimit; + } public void setDiskname1(String string) { diskname1 = string; } public void setDiskname2(String string) { diskname2 = string; } - + public void setComparisonStrategy(int comparisonStrategy) { + this.comparisonStrategy = comparisonStrategy; + } + public void setMessageLimit(int messageLimit) { + this.messageLimit = messageLimit; + } }