
com.webcodepro.applecommander.storage.compare.DiskDiff Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of AppleCommander Show documentation
Show all versions of AppleCommander Show documentation
AppleCommander is a general utility for Apple II disk images.
The newest version!
/*
* 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();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy