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