1package im.conversations.android.xmpp;
2
3import com.google.common.base.Joiner;
4import com.google.common.base.Strings;
5import com.google.common.collect.Collections2;
6import com.google.common.collect.Ordering;
7import com.google.common.hash.HashFunction;
8import com.google.common.hash.Hashing;
9import com.google.common.io.BaseEncoding;
10import com.google.common.primitives.Bytes;
11import eu.siacs.conversations.xml.Namespace;
12import im.conversations.android.xmpp.model.Hash;
13import im.conversations.android.xmpp.model.data.Data;
14import im.conversations.android.xmpp.model.data.Field;
15import im.conversations.android.xmpp.model.data.Value;
16import im.conversations.android.xmpp.model.disco.info.Feature;
17import im.conversations.android.xmpp.model.disco.info.Identity;
18import im.conversations.android.xmpp.model.disco.info.InfoQuery;
19import java.nio.charset.StandardCharsets;
20import java.util.Collection;
21import java.util.Objects;
22
23public class EntityCapabilities2 {
24
25 private static final char UNIT_SEPARATOR = 0x1f;
26 private static final char RECORD_SEPARATOR = 0x1e;
27
28 private static final char GROUP_SEPARATOR = 0x1d;
29
30 private static final char FILE_SEPARATOR = 0x1c;
31
32 public static EntityCaps2Hash hash(final InfoQuery info) {
33 return hash(Hash.Algorithm.SHA_256, info);
34 }
35
36 public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) {
37 final String result = algorithm(info);
38 final var hashFunction = toHashFunction(algorithm);
39 return new EntityCaps2Hash(
40 algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes());
41 }
42
43 private static HashFunction toHashFunction(final Hash.Algorithm algorithm) {
44 return switch (algorithm) {
45 case SHA_1 -> Hashing.sha1();
46 case SHA_256 -> Hashing.sha256();
47 case SHA_512 -> Hashing.sha512();
48 default -> throw new IllegalArgumentException("Unknown hash algorithm");
49 };
50 }
51
52 private static String asHex(final String message) {
53 return Joiner.on(' ')
54 .join(
55 Collections2.transform(
56 Bytes.asList(message.getBytes(StandardCharsets.UTF_8)),
57 b -> String.format("%02x", b)));
58 }
59
60 private static String algorithm(final InfoQuery infoQuery) {
61 return features(infoQuery.getFeatures())
62 + identities(infoQuery.getIdentities())
63 + extensions(infoQuery.getExtensions(Data.class));
64 }
65
66 private static String identities(final Collection<Identity> identities) {
67 return Joiner.on("")
68 .join(
69 Ordering.natural()
70 .sortedCopy(
71 Collections2.transform(
72 identities, EntityCapabilities2::identity)))
73 + FILE_SEPARATOR;
74 }
75
76 private static String identity(final Identity identity) {
77 return Strings.nullToEmpty(identity.getCategory())
78 + UNIT_SEPARATOR
79 + Strings.nullToEmpty(identity.getType())
80 + UNIT_SEPARATOR
81 + Strings.nullToEmpty(identity.getLang())
82 + UNIT_SEPARATOR
83 + Strings.nullToEmpty(identity.getIdentityName())
84 + UNIT_SEPARATOR
85 + RECORD_SEPARATOR;
86 }
87
88 private static String features(Collection<Feature> features) {
89 return Joiner.on("")
90 .join(
91 Ordering.natural()
92 .sortedCopy(
93 Collections2.transform(
94 features, EntityCapabilities2::feature)))
95 + FILE_SEPARATOR;
96 }
97
98 private static String feature(final Feature feature) {
99 return Strings.nullToEmpty(feature.getVar()) + UNIT_SEPARATOR;
100 }
101
102 private static String value(final Value value) {
103 return Strings.nullToEmpty(value.getContent()) + UNIT_SEPARATOR;
104 }
105
106 private static String values(final Collection<Value> values) {
107 return Joiner.on("")
108 .join(
109 Ordering.natural()
110 .sortedCopy(
111 Collections2.transform(
112 values, EntityCapabilities2::value)));
113 }
114
115 private static String field(final Field field) {
116 return Strings.nullToEmpty(field.getFieldName())
117 + UNIT_SEPARATOR
118 + values(field.getExtensions(Value.class))
119 + RECORD_SEPARATOR;
120 }
121
122 private static String fields(final Collection<Field> fields) {
123 return Joiner.on("")
124 .join(
125 Ordering.natural()
126 .sortedCopy(
127 Collections2.transform(
128 fields, EntityCapabilities2::field)))
129 + GROUP_SEPARATOR;
130 }
131
132 private static String extension(final Data data) {
133 return fields(data.getExtensions(Field.class));
134 }
135
136 private static String extensions(final Collection<Data> extensions) {
137 return Joiner.on("")
138 .join(
139 Ordering.natural()
140 .sortedCopy(
141 Collections2.transform(
142 extensions,
143 EntityCapabilities2::extension)))
144 + FILE_SEPARATOR;
145 }
146
147 public static class EntityCaps2Hash extends EntityCapabilities.Hash {
148
149 public final Hash.Algorithm algorithm;
150
151 protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) {
152 super(hash);
153 this.algorithm = algorithm;
154 }
155
156 public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) {
157 return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded));
158 }
159
160 @Override
161 public String capabilityNode(String node) {
162 return String.format(
163 "%s#%s.%s", Namespace.ENTITY_CAPABILITIES_2, algorithm.toString(), encoded());
164 }
165
166 @Override
167 public boolean equals(Object o) {
168 if (this == o) return true;
169 if (o == null || getClass() != o.getClass()) return false;
170 if (!super.equals(o)) return false;
171 EntityCaps2Hash that = (EntityCaps2Hash) o;
172 return algorithm == that.algorithm;
173 }
174
175 @Override
176 public int hashCode() {
177 return Objects.hash(super.hashCode(), algorithm);
178 }
179 }
180}