From fdb96de13f0c69159e88366e10259914cbfb1787 Mon Sep 17 00:00:00 2001 From: Kakhnovich Raman Date: Thu, 30 Jul 2026 17:54:40 +0300 Subject: [PATCH] Add isEquivalentTo method to COSBase and COSObject Add support for object comparison from ISO32000-2:2020 Annex J --- src/main/java/org/verapdf/cos/COSArray.java | 45 ++++++++++ src/main/java/org/verapdf/cos/COSBase.java | 6 ++ src/main/java/org/verapdf/cos/COSBoolean.java | 11 +++ .../java/org/verapdf/cos/COSDictionary.java | 64 ++++++++++++++ .../java/org/verapdf/cos/COSIndirect.java | 16 ++++ src/main/java/org/verapdf/cos/COSInteger.java | 23 +++++ src/main/java/org/verapdf/cos/COSName.java | 14 ++++ src/main/java/org/verapdf/cos/COSNull.java | 5 ++ src/main/java/org/verapdf/cos/COSNumber.java | 4 + src/main/java/org/verapdf/cos/COSObject.java | 13 +++ src/main/java/org/verapdf/cos/COSReal.java | 23 +++++ src/main/java/org/verapdf/cos/COSStream.java | 81 ++++++++++++++++++ src/main/java/org/verapdf/cos/COSString.java | 83 +++++++++++++++++++ 13 files changed, 388 insertions(+) diff --git a/src/main/java/org/verapdf/cos/COSArray.java b/src/main/java/org/verapdf/cos/COSArray.java index a9d02597..fc70cbff 100644 --- a/src/main/java/org/verapdf/cos/COSArray.java +++ b/src/main/java/org/verapdf/cos/COSArray.java @@ -189,6 +189,51 @@ private COSObject _at(final int i) { return this.entries.get(i); } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + if (o == null) return false; + + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get()); + } + if (!(o instanceof COSArray)) return false; + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + @Override + boolean isEquivalentTo(Object o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get(), checkedObjects); + } + if (!(o instanceof COSArray)) return false; + + COSArray that = (COSArray) o; + if (COSBasePair.listContainsPair(checkedObjects, this, that)) { + return true; + } + COSBasePair.addPairToList(checkedObjects, this, that); + + if (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..cbe62cf3 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(Object obj); + + boolean isEquivalentTo(Object 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..8feff328 100644 --- a/src/main/java/org/verapdf/cos/COSBoolean.java +++ b/src/main/java/org/verapdf/cos/COSBoolean.java @@ -71,6 +71,17 @@ public boolean setBoolean(final boolean value) { return true; } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + 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..231e49be 100644 --- a/src/main/java/org/verapdf/cos/COSDictionary.java +++ b/src/main/java/org/verapdf/cos/COSDictionary.java @@ -299,6 +299,70 @@ public Collection getValues() { return this.entries.values(); } + private Set getNonNullKeySet() { + Set nonNullKeys = new HashSet<>(); + for (ASAtom key : getKeySet()) { + COSBase value = getKey(key).get(); // assume getKey returns a COSBase wrapper + if (value != null) { + nonNullKeys.add(key); + } + } + return nonNullKeys; + } + + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + if (o == null) return false; + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get()); + } + if (!(o instanceof COSDictionary)) return false; + + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + /** + * Internal recursive comparison with cycle detection. + */ + boolean isEquivalentTo(Object o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get(), checkedObjects); + } + 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; + } + + for (ASAtom key : thisKeys) { + COSBase thisVal = this.getKey(key).get(); + COSBase thatVal = that.getKey(key).get(); + + 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..abd8c916 100644 --- a/src/main/java/org/verapdf/cos/COSIndirect.java +++ b/src/main/java/org/verapdf/cos/COSIndirect.java @@ -480,6 +480,22 @@ public void mark() { } } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + + if (o instanceof COSIndirect) { + COSIndirect that = (COSIndirect) o; + return this.getDirect().isEquivalentTo(that.getDirect()); + } + + if (o instanceof COSObject) { + return this.getDirect().isEquivalentTo(o); + } + + return false; + } + @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..d0a3711c 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,22 @@ public boolean setReal(final double value) { return true; } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + + 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 +115,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..cb784c3f 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,19 @@ public boolean setName(final ASAtom value) { return true; } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + if (!(o instanceof COSName)) return false; + + COSName that = (COSName) o; + + byte[] thisBytes = this.getName().getValue().getBytes(StandardCharsets.ISO_8859_1); + byte[] thatBytes = that.getName().getValue().getBytes(StandardCharsets.ISO_8859_1); + + return Arrays.equals(thisBytes, thatBytes); + } + 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..e379db66 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(Object 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..1f582c86 100644 --- a/src/main/java/org/verapdf/cos/COSObject.java +++ b/src/main/java/org/verapdf/cos/COSObject.java @@ -494,6 +494,19 @@ public void setIsHeaderFormatComplyPDFA(Boolean isHeaderFormatComplyPDFA) { this.isHeaderFormatComplyPDFA = isHeaderFormatComplyPDFA; } + public boolean isEquivalentTo(Object o) { + if (o == this) return true; + if (o instanceof COSObject) { + COSObject cosObject = (COSObject) o; + return base.isEquivalentTo(cosObject.base); + } + + if (o instanceof COSIndirect) { + return base.isEquivalentTo(((COSIndirect) o).getDirect().base); + } + return false; + } + @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..398f5d49 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,23 @@ public boolean setReal(final double value) { return true; } + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + + 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..15762ddf 100644 --- a/src/main/java/org/verapdf/cos/COSStream.java +++ b/src/main/java/org/verapdf/cos/COSStream.java @@ -283,6 +283,87 @@ 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).get() == null); + return keys; + } + + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + if (o == null) return false; + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get()); + } + if (!(o instanceof COSStream)) return false; + List checkedObjects = new LinkedList<>(); + return isEquivalentTo(o, checkedObjects); + } + + @Override + boolean isEquivalentTo(Object o, List checkedObjects) { + if (this == o) return true; + if (o == null) return false; + if (o instanceof COSObject) { + return this.isEquivalentTo(((COSObject) o).get(), checkedObjects); + } + 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", e); + 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; + } + for (ASAtom key : thisKeys) { + COSBase thisVal = this.getKey(key).get(); + COSBase thatVal = that.getKey(key).get(); + if (thisVal == null && thatVal == null) continue; + if (thisVal == null || thatVal == null) return false; + if (!thisVal.isEquivalentTo(thatVal, checkedObjects)) { + return false; + } + } + + return true; + } + + 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..0128fc00 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,88 @@ public void setHexCount(long hexCount) { this.hexCount = hexCount; } + private byte[] getCanonicalBytes() { + if (isHex) { + return Arrays.copyOf(value, value.length); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int i = 0; + while (i < value.length) { + byte b = value[i]; + if (b == '\\') { + if (i + 1 >= value.length) { + break; + } + byte next = value[i + 1]; + + if (next >= '0' && next <= '7') { + int octal = 0; + int count = 0; + while (i + 1 < value.length && count < 3) { + byte c = value[i + 1]; + if (c >= '0' && c <= '7') { + octal = octal * 8 + (c - '0'); + i++; + count++; + } else { + break; + } + } + out.write(octal); + continue; + } + + if (next == '\r') { + i += 2; + if (i < value.length && value[i] == '\n') { + i++; + } + continue; + } else if (next == '\n') { + i += 2; + continue; + } + + i++; + if (i >= value.length) break; + byte esc = value[i]; + byte replacement; + switch (esc) { + case 'n': replacement = '\n'; break; + case 'r': replacement = '\r'; break; + case 't': replacement = '\t'; break; + case 'b': replacement = '\b'; break; + case 'f': replacement = '\f'; break; + case '(': replacement = '('; break; + case ')': replacement = ')'; break; + case '\\': replacement = '\\'; break; + default: + replacement = esc; + break; + } + out.write(replacement); + } else { + out.write(b); + } + i++; + } + return out.toByteArray(); + } + + @Override + public boolean isEquivalentTo(Object o) { + if (this == o) return true; + if (!(o instanceof COSString)) return false; + + COSString that = (COSString) o; + + byte[] thisBytes = this.getCanonicalBytes(); + byte[] thatBytes = that.getCanonicalBytes(); + + return Arrays.equals(thisBytes, thatBytes); + } + @Override public boolean equals(Object o) { if (this == o) return true;