migrate entity caps 1 calculation to new code

This commit is contained in:
Daniel Gultsch 2023-01-16 12:24:51 +01:00
parent 482dc8cfe9
commit 78af8cbd87
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
8 changed files with 230 additions and 0 deletions

View file

@ -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<Identity> orderedIdentities =
Ordering.from(
(Comparator<Identity>)
(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<String> features =
Ordering.natural()
.sortedCopy(
Collections2.transform(
info.getExtensions(Feature.class), Feature::getVar));
for (final String feature : features) {
s.append(clean(feature)).append("<");
}
final List<Data> 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<Field> 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<String> 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("<", "&lt;");
}
private static String blankNull(String s) {
return s == null ? "" : clean(s);
}
}

View file

@ -25,6 +25,7 @@ public final class Extensions {
im.conversations.android.xmpp.model.blocking.Unblock.class, im.conversations.android.xmpp.model.blocking.Unblock.class,
im.conversations.android.xmpp.model.data.Data.class, im.conversations.android.xmpp.model.data.Data.class,
im.conversations.android.xmpp.model.data.Field.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.Feature.class,
im.conversations.android.xmpp.model.disco.info.Identity.class, im.conversations.android.xmpp.model.disco.info.Identity.class,
im.conversations.android.xmpp.model.disco.info.InfoQuery.class, im.conversations.android.xmpp.model.disco.info.InfoQuery.class,

View file

@ -1,11 +1,28 @@
package im.conversations.android.xmpp.model.data; 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.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@XmlElement(name = "x") @XmlElement(name = "x")
public class Data extends Extension { public class Data extends Extension {
private static final String FORM_TYPE = "FORM_TYPE";
public Data() { public Data() {
super(Data.class); 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<Field> getFields() {
return Collections2.filter(
this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName()));
}
} }

View file

@ -1,11 +1,22 @@
package im.conversations.android.xmpp.model.data; 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.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@XmlElement @XmlElement
public class Field extends Extension { public class Field extends Extension {
public Field() { public Field() {
super(Field.class); super(Field.class);
} }
public String getFieldName() {
return getAttribute("var");
}
public Collection<String> getValues() {
return Collections2.transform(getExtensions(Value.class), Element::getContent);
}
} }

View file

@ -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);
}
}

View file

@ -8,4 +8,8 @@ public class Feature extends Extension {
public Feature() { public Feature() {
super(Feature.class); super(Feature.class);
} }
public String getVar() {
return this.getAttribute("var");
}
} }

View file

@ -8,4 +8,20 @@ public class Identity extends Extension {
public Identity() { public Identity() {
super(Identity.class); 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");
}
} }

View file

@ -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 =
"<query xmlns='http://jabber.org/protocol/disco#info'\n"
+ " "
+ " node='http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0='>\n"
+ " <identity category='client' name='Exodus 0.9.1' type='pc'/>\n"
+ " <feature var='http://jabber.org/protocol/caps'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#info'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#items'/>\n"
+ " <feature var='http://jabber.org/protocol/muc'/>\n"
+ " </query>";
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 =
"<query xmlns='http://jabber.org/protocol/disco#info'\n"
+ " node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>\n"
+ " <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>\n"
+ " <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>\n"
+ " <feature var='http://jabber.org/protocol/caps'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#info'/>\n"
+ " <feature var='http://jabber.org/protocol/disco#items'/>\n"
+ " <feature var='http://jabber.org/protocol/muc'/>\n"
+ " <x xmlns='jabber:x:data' type='result'>\n"
+ " <field var='FORM_TYPE' type='hidden'>\n"
+ " <value>urn:xmpp:dataforms:softwareinfo</value>\n"
+ " </field>\n"
+ " <field var='ip_version' type='text-multi' >\n"
+ " <value>ipv4</value>\n"
+ " <value>ipv6</value>\n"
+ " </field>\n"
+ " <field var='os'>\n"
+ " <value>Mac</value>\n"
+ " </field>\n"
+ " <field var='os_version'>\n"
+ " <value>10.5.1</value>\n"
+ " </field>\n"
+ " <field var='software'>\n"
+ " <value>Psi</value>\n"
+ " </field>\n"
+ " <field var='software_version'>\n"
+ " <value>0.11</value>\n"
+ " </field>\n"
+ " </x>\n"
+ " </query>";
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);
}
}