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}