mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2024-10-11 09:23:43 +00:00
Merge pull request #78 from AppleCommander/feature/expand_compare_capabilities
Feature/expand compare capabilities #48
This commit is contained in:
commit
7bb1408b3a
@ -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,
|
||||
|
@ -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<Integer> 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<DiskDiff.Builder> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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<String> errors = new ArrayList<>();
|
||||
private List<String> warnings = new ArrayList<>();
|
||||
|
||||
public boolean hasErrors() {
|
||||
return !errors.isEmpty();
|
||||
}
|
||||
public int getDifferenceCount() {
|
||||
return errors.size() + warnings.size();
|
||||
}
|
||||
|
||||
public List<String> getAllMessages() {
|
||||
List<String> messages = new ArrayList<>();
|
||||
messages.addAll(errors);
|
||||
messages.addAll(warnings);
|
||||
return messages;
|
||||
}
|
||||
public List<String> getLimitedMessages(int limit) {
|
||||
List<String> 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<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
}
|
@ -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<FormattedDisk,FormattedDisk> 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<min; i++) {
|
||||
this.diskComparisonStrategy.accept(formattedDisksA[i], formattedDisksB[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare disks by whatever native geometry the disks have. Fails if geometries do not match. */
|
||||
public void compareByNativeGeometry(FormattedDisk formattedDiskA, FormattedDisk formattedDiskB) {
|
||||
DiskGeometry geometryA = formattedDiskA.getDiskGeometry();
|
||||
DiskGeometry geometryB = formattedDiskB.getDiskGeometry();
|
||||
|
||||
if (geometryA != geometryB) {
|
||||
results.addError("Disks are different geometry (block versus track/sector)");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (geometryA) {
|
||||
case BLOCK:
|
||||
compareByBlockGeometry(formattedDiskA, formattedDiskB);
|
||||
break;
|
||||
case TRACK_SECTOR:
|
||||
compareByTrackSectorGeometry(formattedDiskA, formattedDiskB);
|
||||
break;
|
||||
default:
|
||||
results.addError("Unknown geometry: %s", geometryA);
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare disks by 512-byte ProDOS/Pascal blocks. */
|
||||
public void compareByBlockGeometry(FormattedDisk formattedDiskA, FormattedDisk formattedDiskB) {
|
||||
ImageOrder orderA = formattedDiskA.getImageOrder();
|
||||
ImageOrder orderB = formattedDiskB.getImageOrder();
|
||||
|
||||
if (orderA.getBlocksOnDevice() != orderB.getBlocksOnDevice()) {
|
||||
results.addError("Different sized disks do not equal. (Blocks: %d <> %d)",
|
||||
orderA.getBlocksOnDevice(), orderB.getBlocksOnDevice());
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> unequalBlocks = new ArrayList<>();
|
||||
for (int block=0; block<orderA.getBlocksOnDevice(); block++) {
|
||||
byte[] blockA = orderA.readBlock(block);
|
||||
byte[] blockB = orderB.readBlock(block);
|
||||
if (!Arrays.equals(blockA, blockB)) {
|
||||
unequalBlocks.add(block);
|
||||
}
|
||||
}
|
||||
for (Range r : Range.from(unequalBlocks)) {
|
||||
if (r.size() == 1) {
|
||||
results.addError("Block #%s does not match.", r);
|
||||
}
|
||||
else {
|
||||
results.addError("Blocks #%s do not match.", r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare disks by 256-byte DOS sectors. */
|
||||
public void compareByTrackSectorGeometry(FormattedDisk formattedDiskA, FormattedDisk formattedDiskB) {
|
||||
ImageOrder orderA = formattedDiskA.getImageOrder();
|
||||
ImageOrder orderB = formattedDiskB.getImageOrder();
|
||||
|
||||
if (orderA.getSectorsPerDisk() != orderB.getSectorsPerDisk()) {
|
||||
results.addError("Different sized disks do not equal. (Sectors: %d <> %d)",
|
||||
orderA.getSectorsPerDisk(), orderB.getSectorsPerDisk());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int track=0; track<orderA.getTracksPerDisk(); track++) {
|
||||
List<Integer> unequalSectors = new ArrayList<>();
|
||||
for (int sector=0; sector<orderA.getSectorsPerTrack(); sector++) {
|
||||
byte[] sectorA = orderA.readSector(track, sector);
|
||||
byte[] sectorB = orderB.readSector(track, sector);
|
||||
if (!Arrays.equals(sectorA, sectorB)) {
|
||||
unequalSectors.add(sector);
|
||||
}
|
||||
}
|
||||
if (!unequalSectors.isEmpty()) {
|
||||
results.addError("Track %d does not match on sectors %s", track,
|
||||
Range.from(unequalSectors)
|
||||
.stream()
|
||||
.map(Range::toString)
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare by filename. This accounts for names only in disk A, only in disk B, or different but same-named. */
|
||||
public void compareByFileName(FormattedDisk formattedDiskA, FormattedDisk formattedDiskB) {
|
||||
try {
|
||||
Map<String,List<FileTuple>> filesA = FileStreamer.forDisk(formattedDiskA)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(FileTuple::fullPath));
|
||||
Map<String,List<FileTuple>> filesB = FileStreamer.forDisk(formattedDiskB)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(FileTuple::fullPath));
|
||||
|
||||
Set<String> 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<String> 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<String> pathsInAB = new HashSet<>(filesA.keySet());
|
||||
pathsInAB.retainAll(filesB.keySet());
|
||||
for (String path : pathsInAB) {
|
||||
List<FileTuple> tuplesA = filesA.get(path);
|
||||
List<FileTuple> 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<String> 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<String,List<FileTuple>> contentA = FileStreamer.forDisk(formattedDiskA)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(this::contentHash));
|
||||
Map<String,List<FileTuple>> contentB = FileStreamer.forDisk(formattedDiskB)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(this::contentHash));
|
||||
|
||||
Set<String> contentOnlyA = new HashSet<>(contentA.keySet());
|
||||
contentOnlyA.removeAll(contentB.keySet());
|
||||
if (!contentOnlyA.isEmpty()) {
|
||||
Set<String> 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<String> contentOnlyB = new HashSet<>(contentB.keySet());
|
||||
contentOnlyB.removeAll(contentA.keySet());
|
||||
if (!contentOnlyB.isEmpty()) {
|
||||
Set<String> 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<String> contentInAB = new HashSet<>(contentA.keySet());
|
||||
contentInAB.retainAll(contentB.keySet());
|
||||
for (String content : contentInAB) {
|
||||
List<FileTuple> tuplesA = contentA.get(content);
|
||||
List<FileTuple> 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<String> 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<String> compare(FileEntryReader readerA, FileEntryReader readerB) {
|
||||
List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<Range> from(List<Integer> numbers) {
|
||||
List<Range> 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;
|
||||
}
|
||||
}
|
@ -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<PathMatcher> 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) {
|
||||
|
@ -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<String> 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<String>(), (DirectoryEntry)disk, null);
|
||||
|
@ -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;
|
@ -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<Date> 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);
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
|
||||