All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.webcodepro.applecommander.storage.compare.DiskDiff Maven / Gradle / Ivy

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