From 78af8cbd875acd60bbc036d182ea2bf3bf1646ca Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 16 Jan 2023 12:24:51 +0100 Subject: [PATCH] migrate entity caps 1 calculation to new code --- .../android/xmpp/EntityCapabilities.java | 89 +++++++++++++++++++ .../android/xmpp/Extensions.java | 1 + .../android/xmpp/model/data/Data.java | 17 ++++ .../android/xmpp/model/data/Field.java | 11 +++ .../android/xmpp/model/data/Value.java | 12 +++ .../xmpp/model/disco/info/Feature.java | 4 + .../xmpp/model/disco/info/Identity.java | 16 ++++ .../android/xmpp/EntityCapabilitiesTest.java | 80 +++++++++++++++++ 8 files changed, 230 insertions(+) create mode 100644 src/main/java/im/conversations/android/xmpp/EntityCapabilities.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/data/Value.java create mode 100644 src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java new file mode 100644 index 000000000..e9da963da --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java @@ -0,0 +1,89 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Ordering; +import com.google.common.hash.Hashing; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; + +public final class EntityCapabilities { + public static byte[] hash(final InfoQuery info) { + final StringBuilder s = new StringBuilder(); + final List orderedIdentities = + Ordering.from( + (Comparator) + (a, b) -> + ComparisonChain.start() + .compare( + blankNull(a.getCategory()), + blankNull(b.getCategory())) + .compare( + blankNull(a.getType()), + blankNull(b.getType())) + .compare( + blankNull(a.getLang()), + blankNull(b.getLang())) + .compare( + blankNull(a.getIdentityName()), + blankNull(b.getIdentityName())) + .result()) + .sortedCopy(info.getExtensions(Identity.class)); + + for (final Identity id : orderedIdentities) { + s.append(blankNull(id.getCategory())) + .append("/") + .append(blankNull(id.getType())) + .append("/") + .append(blankNull(id.getLang())) + .append("/") + .append(blankNull(id.getIdentityName())) + .append("<"); + } + + final List features = + Ordering.natural() + .sortedCopy( + Collections2.transform( + info.getExtensions(Feature.class), Feature::getVar)); + for (final String feature : features) { + s.append(clean(feature)).append("<"); + } + + final List extensions = + Ordering.from(Comparator.comparing(Data::getFormType)) + .sortedCopy(info.getExtensions(Data.class)); + + for (final Data extension : extensions) { + s.append(clean(extension.getFormType())).append("<"); + final List fields = + Ordering.from( + Comparator.comparing( + (Field lhs) -> Strings.nullToEmpty(lhs.getFieldName()))) + .sortedCopy(extension.getFields()); + for (final Field field : fields) { + s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); + final List values = Ordering.natural().sortedCopy(field.getValues()); + for (final String value : values) { + s.append(blankNull(value)).append("<"); + } + } + } + return Hashing.sha1().hashString(s.toString(), StandardCharsets.UTF_8).asBytes(); + } + + private static String clean(String s) { + return s.replace("<", "<"); + } + + private static String blankNull(String s) { + return s == null ? "" : clean(s); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Extensions.java b/src/main/java/im/conversations/android/xmpp/Extensions.java index 184291976..6e7f7ac9e 100644 --- a/src/main/java/im/conversations/android/xmpp/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/Extensions.java @@ -25,6 +25,7 @@ public final class Extensions { im.conversations.android.xmpp.model.blocking.Unblock.class, im.conversations.android.xmpp.model.data.Data.class, im.conversations.android.xmpp.model.data.Field.class, + im.conversations.android.xmpp.model.data.Value.class, im.conversations.android.xmpp.model.disco.info.Feature.class, im.conversations.android.xmpp.model.disco.info.Identity.class, im.conversations.android.xmpp.model.disco.info.InfoQuery.class, diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index 2d72d04f4..582727718 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -1,11 +1,28 @@ package im.conversations.android.xmpp.model.data; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement(name = "x") public class Data extends Extension { + + private static final String FORM_TYPE = "FORM_TYPE"; + public Data() { super(Data.class); } + + public String getFormType() { + final var fields = this.getExtensions(Field.class); + final var formTypeField = Iterables.find(fields, f -> FORM_TYPE.equals(f.getFieldName())); + return Iterables.getFirst(formTypeField.getValues(), null); + } + + public Collection getFields() { + return Collections2.filter( + this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index 14a11cec5..f59398254 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -1,11 +1,22 @@ package im.conversations.android.xmpp.model.data; +import com.google.common.collect.Collections2; +import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement public class Field extends Extension { public Field() { super(Field.class); } + + public String getFieldName() { + return getAttribute("var"); + } + + public Collection getValues() { + return Collections2.transform(getExtensions(Value.class), Element::getContent); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Value.java b/src/main/java/im/conversations/android/xmpp/model/data/Value.java new file mode 100644 index 000000000..8e9eccc4d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Value.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.data; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Value extends Extension { + + public Value() { + super(Value.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java index 1016b99ac..191c45e60 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java @@ -8,4 +8,8 @@ public class Feature extends Extension { public Feature() { super(Feature.class); } + + public String getVar() { + return this.getAttribute("var"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java index cbacdf26a..6b5658a89 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java @@ -8,4 +8,20 @@ public class Identity extends Extension { public Identity() { super(Identity.class); } + + public String getCategory() { + return this.getAttribute("category"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getLang() { + return this.getAttribute("xml:lang"); + } + + public String getIdentityName() { + return this.getAttribute("name"); + } } diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java new file mode 100644 index 000000000..f2a1d3e05 --- /dev/null +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -0,0 +1,80 @@ +package im.conversations.android.xmpp; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.common.io.BaseEncoding; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.XmlElementReader; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class EntityCapabilitiesTest { + + @Test + public void entityCaps() throws IOException { + final String xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = BaseEncoding.base64().encode(EntityCapabilities.hash(info)); + Assert.assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", var); + } + + @Test + public void entityCapsComplexExample() throws IOException { + final String xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " urn:xmpp:dataforms:softwareinfo\n" + + " \n" + + " \n" + + " ipv4\n" + + " ipv6\n" + + " \n" + + " \n" + + " Mac\n" + + " \n" + + " \n" + + " 10.5.1\n" + + " \n" + + " \n" + + " Psi\n" + + " \n" + + " \n" + + " 0.11\n" + + " \n" + + " \n" + + " "; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = BaseEncoding.base64().encode(EntityCapabilities.hash(info)); + Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var); + } +}