diff --git a/src/main/java/org/verapdf/cos/COSArray.java b/src/main/java/org/verapdf/cos/COSArray.java index a9d02597..3e8cf1e2 100644 --- a/src/main/java/org/verapdf/cos/COSArray.java +++ b/src/main/java/org/verapdf/cos/COSArray.java @@ -189,6 +189,49 @@ private COSObject _at(final int i) { return this.entries.get(i); } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return this.isEquivalentTo(o.getDirectBase()); + } + + if (!(o instanceof COSArray)) return false; + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + @Override + boolean isEquivalentTo(COSBase o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + + if (!(o instanceof COSArray)) return false; + + COSArray that = (COSArray) o; + if (COSBasePair.listContainsPair(checkedObjects, this, that)) { + return true; + } + COSBasePair.addPairToList(checkedObjects, this, that); + + if (!Objects.equals(this.size(), that.size())) return false; + + for (int i = 0; i < this.size(); ++i) { + COSBase thisElem = this.at(i).getDirectBase(); + COSBase thatElem = that.at(i).getDirectBase(); + + if (thisElem == null && thatElem == null) continue; + if (thisElem == null || thatElem == null) return false; + + if (!thisElem.isEquivalentTo(thatElem, checkedObjects)) { + return false; + } + } + return true; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/main/java/org/verapdf/cos/COSBase.java b/src/main/java/org/verapdf/cos/COSBase.java index 4c1ee154..7378f1dd 100644 --- a/src/main/java/org/verapdf/cos/COSBase.java +++ b/src/main/java/org/verapdf/cos/COSBase.java @@ -140,6 +140,12 @@ public void setObjectKey(COSKey indirectKey) { public abstract void mark(); + public abstract boolean isEquivalentTo(COSBase obj); + + boolean isEquivalentTo(COSBase o, List checkedObjects) { + return isEquivalentTo(o); + } + boolean equals(Object obj, List checkedObjects) { return this.equals(obj); } diff --git a/src/main/java/org/verapdf/cos/COSBoolean.java b/src/main/java/org/verapdf/cos/COSBoolean.java index 1cae11d2..3ba44c4d 100644 --- a/src/main/java/org/verapdf/cos/COSBoolean.java +++ b/src/main/java/org/verapdf/cos/COSBoolean.java @@ -71,6 +71,23 @@ public boolean setBoolean(final boolean value) { return true; } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (!(o instanceof COSBoolean)) return false; + + COSBoolean that = (COSBoolean) o; + + return value == that.value; + + } + public boolean get() { return this.value; } diff --git a/src/main/java/org/verapdf/cos/COSDictionary.java b/src/main/java/org/verapdf/cos/COSDictionary.java index 057088c8..aa7d8108 100644 --- a/src/main/java/org/verapdf/cos/COSDictionary.java +++ b/src/main/java/org/verapdf/cos/COSDictionary.java @@ -299,6 +299,73 @@ public Collection getValues() { return this.entries.values(); } + private Set getNonNullKeySet() { + Set nonNullKeys = new HashSet<>(); + for (ASAtom key : getKeySet()) { + COSBase value = getKey(key).getDirectBase(); + if (value != null) { + nonNullKeys.add(key); + } + } + return nonNullKeys; + } + + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (!(o instanceof COSDictionary)) return false; + + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + /** + * Internal recursive comparison with cycle detection. + */ + boolean isEquivalentTo(COSBase o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof COSDictionary)) return false; + + COSDictionary that = (COSDictionary) o; + + if (COSBasePair.listContainsPair(checkedObjects, this, that)) { + return true; + } + COSBasePair.addPairToList(checkedObjects, this, that); + + Set thisKeys = this.getNonNullKeySet(); + Set thatKeys = that.getNonNullKeySet(); + + if (!thisKeys.equals(thatKeys)) { + return false; + } + + return isEquivalentKeys(that, thisKeys, checkedObjects); + } + + protected boolean isEquivalentKeys(COSDictionary that, Set thisKeys, List checkedObjects) { + for (ASAtom key : thisKeys) { + COSBase thisVal = this.getKey(key).getDirectBase(); + COSBase thatVal = that.getKey(key).getDirectBase(); + + if (thisVal == null && thatVal == null) continue; + if (thisVal == null || thatVal == null) return false; + + if (!thisVal.isEquivalentTo(thatVal, checkedObjects)) { + return false; + } + } + + return true; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/main/java/org/verapdf/cos/COSIndirect.java b/src/main/java/org/verapdf/cos/COSIndirect.java index 343fa197..9b3f67a4 100644 --- a/src/main/java/org/verapdf/cos/COSIndirect.java +++ b/src/main/java/org/verapdf/cos/COSIndirect.java @@ -480,6 +480,17 @@ public void mark() { } } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + + COSBase thisBase = this.getDirectBase(); + if (thisBase == null && o == null) return true; + if (thisBase == null || o == null) return false; + + return thisBase.isEquivalentTo(o.getDirectBase()); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/org/verapdf/cos/COSInteger.java b/src/main/java/org/verapdf/cos/COSInteger.java index f3acdddd..899e13e3 100644 --- a/src/main/java/org/verapdf/cos/COSInteger.java +++ b/src/main/java/org/verapdf/cos/COSInteger.java @@ -23,6 +23,8 @@ import org.verapdf.cos.visitor.ICOSVisitor; import org.verapdf.cos.visitor.IVisitor; +import java.math.BigDecimal; + /** * @author Timur Kamalov */ @@ -75,6 +77,27 @@ public boolean setReal(final double value) { return true; } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (o instanceof COSInteger) { + return this.value == ((COSInteger) o).value; + } + + if (o instanceof COSReal) { + COSReal thatReal = (COSReal) o; + return BigDecimal.valueOf(this.value).compareTo(thatReal.getDecimalValue()) == 0; + } + + return false; + } + public long get() { return this.value; } @@ -97,4 +120,9 @@ public boolean equals(Object o) { return value == that.value; } + + @Override + public BigDecimal getDecimalValue() { + return BigDecimal.valueOf(value); + } } diff --git a/src/main/java/org/verapdf/cos/COSName.java b/src/main/java/org/verapdf/cos/COSName.java index 41d7ef11..fc2b8c93 100644 --- a/src/main/java/org/verapdf/cos/COSName.java +++ b/src/main/java/org/verapdf/cos/COSName.java @@ -25,6 +25,7 @@ import org.verapdf.cos.visitor.IVisitor; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Objects; /** @@ -91,6 +92,22 @@ public boolean setName(final ASAtom value) { return true; } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (!(o instanceof COSName)) return false; + + COSName that = (COSName) o; + + return Objects.equals(value, that.value); + } + public ASAtom get() { return value; } diff --git a/src/main/java/org/verapdf/cos/COSNull.java b/src/main/java/org/verapdf/cos/COSNull.java index c8f63e53..e159a280 100644 --- a/src/main/java/org/verapdf/cos/COSNull.java +++ b/src/main/java/org/verapdf/cos/COSNull.java @@ -51,6 +51,11 @@ public Object accept(final ICOSVisitor visitor) { return visitor.visitFromNull(this); } + @Override + public boolean isEquivalentTo(COSBase o) { + return equals(o); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/org/verapdf/cos/COSNumber.java b/src/main/java/org/verapdf/cos/COSNumber.java index f970c762..009e64db 100644 --- a/src/main/java/org/verapdf/cos/COSNumber.java +++ b/src/main/java/org/verapdf/cos/COSNumber.java @@ -20,6 +20,8 @@ */ package org.verapdf.cos; +import java.math.BigDecimal; + /** * @author Timur Kamalov */ @@ -29,4 +31,6 @@ public COSNumber() { super(); } + public abstract BigDecimal getDecimalValue(); + } diff --git a/src/main/java/org/verapdf/cos/COSObject.java b/src/main/java/org/verapdf/cos/COSObject.java index f23a4916..fad8c777 100644 --- a/src/main/java/org/verapdf/cos/COSObject.java +++ b/src/main/java/org/verapdf/cos/COSObject.java @@ -494,6 +494,18 @@ public void setIsHeaderFormatComplyPDFA(Boolean isHeaderFormatComplyPDFA) { this.isHeaderFormatComplyPDFA = isHeaderFormatComplyPDFA; } + public boolean isEquivalentTo(COSObject o) { + if (o == this) return true; + if (o == null) return false; + + COSBase thisBase = this.getDirectBase(); + COSBase otherBase = o.getDirectBase(); + if (thisBase == null && otherBase == null) return true; + if (thisBase == null || otherBase == null) return false; + + return thisBase.isEquivalentTo(otherBase); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/org/verapdf/cos/COSReal.java b/src/main/java/org/verapdf/cos/COSReal.java index d43d87bf..244b20fb 100644 --- a/src/main/java/org/verapdf/cos/COSReal.java +++ b/src/main/java/org/verapdf/cos/COSReal.java @@ -23,6 +23,7 @@ import org.verapdf.cos.visitor.ICOSVisitor; import org.verapdf.cos.visitor.IVisitor; +import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -45,6 +46,11 @@ public class COSReal extends COSNumber { protected COSReal() { } + @Override + public BigDecimal getDecimalValue() { + return BigDecimal.valueOf(value); + } + protected COSReal(final double value) { this.value = value; } @@ -88,6 +94,27 @@ public boolean setReal(final double value) { return true; } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (o instanceof COSReal) { + COSReal thatReal = (COSReal) o; + return this.getDecimalValue().compareTo(thatReal.getDecimalValue()) == 0; + } + + if (o instanceof COSInteger) { + COSInteger thatInt = (COSInteger) o; + return this.getDecimalValue().compareTo(thatInt.getDecimalValue()) == 0; + } + + return false; + } + public double get() { return this.value; } diff --git a/src/main/java/org/verapdf/cos/COSStream.java b/src/main/java/org/verapdf/cos/COSStream.java index a6472545..8fc4976e 100644 --- a/src/main/java/org/verapdf/cos/COSStream.java +++ b/src/main/java/org/verapdf/cos/COSStream.java @@ -283,6 +283,76 @@ public enum FilterFlags { DECRYPT_AND_DECODE } + private Set getNonNullKeysExcluding(Set exclude) { + Set keys = new HashSet<>(this.getKeySet()); + keys.removeAll(exclude); + keys.removeIf(key -> this.getKey(key).getDirectBase() == null); + return keys; + } + + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + if (!(o instanceof COSStream)) return false; + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + @Override + boolean isEquivalentTo(COSBase o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + + if (!(o instanceof COSStream)) return false; + COSStream that = (COSStream) o; + + if (COSBasePair.listContainsPair(checkedObjects, this, that)) { + return true; + } + COSBasePair.addPairToList(checkedObjects, this, that); + + try { + ASInputStream thisDecoded = this.getData(FilterFlags.DECODE); + ASInputStream thatDecoded = that.getData(FilterFlags.DECODE); + if (!equalsDecodedStreams(thisDecoded, thatDecoded)) { + return false; + } + } catch (IOException e) { + LOGGER.log(Level.FINE, "Exception during comparing decoded streams"); + return false; + } + + Set excluded = new HashSet<>(Arrays.asList(ASAtom.LENGTH, ASAtom.FILTER, ASAtom.DECODE_PARMS)); + Set thisKeys = this.getNonNullKeysExcluding(excluded); + Set thatKeys = that.getNonNullKeysExcluding(excluded); + + if (!thisKeys.equals(thatKeys)) { + return false; + } + + return isEquivalentKeys(that, thisKeys, checkedObjects); + } + + private static boolean equalsDecodedStreams(ASInputStream first, ASInputStream second) throws IOException { + byte[] buf1 = new byte[8192]; + byte[] buf2 = new byte[8192]; + int read1, read2; + while (true) { + read1 = first.read(buf1); + read2 = second.read(buf2); + if (read1 != read2) return false; + if (read1 == -1) break; + for (int i = 0; i < read1; i++) { + if (buf1[i] != buf2[i]) return false; + } + } + return true; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/main/java/org/verapdf/cos/COSString.java b/src/main/java/org/verapdf/cos/COSString.java index db633e67..94940cf7 100644 --- a/src/main/java/org/verapdf/cos/COSString.java +++ b/src/main/java/org/verapdf/cos/COSString.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; +import java.io.ByteArrayOutputStream; /** * @author Timur Kamalov @@ -364,6 +365,25 @@ public void setHexCount(long hexCount) { this.hexCount = hexCount; } + @Override + public boolean isEquivalentTo(COSBase o) { + if (this == o) return true; + if (o == null) return false; + + if (o.isIndirect()) { + return isEquivalentTo(o.getDirectBase()); + } + + if (!(o instanceof COSString)) return false; + + COSString that = (COSString) o; + + String thisString = this.getString(); + String thatString = that.getString(); + + return thisString.equals(thatString); + } + @Override public boolean equals(Object o) { if (this == o) return true;