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}