FileTransferDescription.java

  1package eu.siacs.conversations.xmpp.jingle.stanzas;
  2
  3import android.util.Log;
  4
  5import androidx.annotation.NonNull;
  6
  7import com.google.common.base.CaseFormat;
  8import com.google.common.base.MoreObjects;
  9import com.google.common.base.Preconditions;
 10import com.google.common.base.Strings;
 11import com.google.common.collect.ImmutableList;
 12import com.google.common.io.BaseEncoding;
 13import com.google.common.primitives.Longs;
 14
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.xml.Element;
 17import eu.siacs.conversations.xml.Namespace;
 18import im.conversations.android.xmpp.model.jingle.Jingle;
 19
 20import java.util.List;
 21
 22public class FileTransferDescription extends GenericDescription {
 23
 24    private FileTransferDescription() {
 25        super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
 26    }
 27
 28    public static FileTransferDescription of(final File fileDescription) {
 29        final var description = new FileTransferDescription();
 30        final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
 31        file.addChild("name").setContent(fileDescription.name);
 32        file.addChild("size").setContent(Long.toString(fileDescription.size));
 33        if (fileDescription.mediaType != null) {
 34            file.addChild("mediaType").setContent(fileDescription.mediaType);
 35        }
 36        return description;
 37    }
 38
 39    public File getFile() {
 40        final Element fileElement = this.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
 41        if (fileElement == null) {
 42            Log.d(Config.LOGTAG,"no file? "+this);
 43            throw new IllegalStateException("file transfer description has no file");
 44        }
 45        final String name = fileElement.findChildContent("name");
 46        final String sizeAsString = fileElement.findChildContent("size");
 47        final String mediaType = fileElement.findChildContent("mediaType");
 48        if (Strings.isNullOrEmpty(name) || Strings.isNullOrEmpty(sizeAsString)) {
 49            throw new IllegalStateException("File definition is missing name and/or size");
 50        }
 51        final Long size = Longs.tryParse(sizeAsString);
 52        if (size == null) {
 53            throw new IllegalStateException("Invalid file size");
 54        }
 55        final List<Hash> hashes = findHashes(fileElement.getChildren());
 56        return new File(size, name, mediaType, hashes);
 57    }
 58
 59    public static SessionInfo getSessionInfo(@NonNull final Jingle jingle) {
 60        Preconditions.checkNotNull(jingle);
 61        Preconditions.checkArgument(
 62                jingle.getAction() == Jingle.Action.SESSION_INFO,
 63                "jingle packet is not a session-info");
 64        final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
 65        if (checksum != null) {
 66            final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
 67            final String name = checksum.getAttribute("name");
 68            if (file == null || Strings.isNullOrEmpty(name)) {
 69                return null;
 70            }
 71            return new Checksum(name, findHashes(file.getChildren()));
 72        }
 73        final Element received = jingle.findChild("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
 74        if (received != null) {
 75            final String name = received.getAttribute("name");
 76            if (Strings.isNullOrEmpty(name)) {
 77                return new Received(name);
 78            }
 79        }
 80        return null;
 81    }
 82
 83    private static List<Hash> findHashes(final List<Element> elements) {
 84        final ImmutableList.Builder<Hash> hashes = new ImmutableList.Builder<>();
 85        for (final Element child : elements) {
 86            if ("hash".equals(child.getName()) && Namespace.HASHES.equals(child.getNamespace())) {
 87                final Algorithm algorithm;
 88                try {
 89                    algorithm = Algorithm.of(child.getAttribute("algo"));
 90                } catch (final IllegalArgumentException e) {
 91                    continue;
 92                }
 93                final String content = child.getContent();
 94                if (Strings.isNullOrEmpty(content)) {
 95                    continue;
 96                }
 97                if (BaseEncoding.base64().canDecode(content)) {
 98                    hashes.add(new Hash(BaseEncoding.base64().decode(content), algorithm));
 99                }
100            }
101        }
102        return hashes.build();
103    }
104
105    public static FileTransferDescription upgrade(final Element element) {
106        Preconditions.checkArgument(
107                "description".equals(element.getName()),
108                "Name of provided element is not description");
109        Preconditions.checkArgument(
110                element.getNamespace().equals(Namespace.JINGLE_APPS_FILE_TRANSFER),
111                "Element does not match a file transfer namespace");
112        final FileTransferDescription description = new FileTransferDescription();
113        description.setAttributes(element.getAttributes());
114        description.setChildren(element.getChildren());
115        return description;
116    }
117
118    public static final class Checksum extends SessionInfo {
119        public final List<Hash> hashes;
120
121        public Checksum(final String name, List<Hash> hashes) {
122            super(name);
123            this.hashes = hashes;
124        }
125
126        @Override
127        @NonNull
128        public String toString() {
129            return MoreObjects.toStringHelper(this).add("hashes", hashes).toString();
130        }
131
132        @Override
133        public Element asElement() {
134            final var checksum = new Element("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
135            checksum.setAttribute("name", name);
136            final var file = checksum.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
137            for (final Hash hash : hashes) {
138                final var element = file.addChild("hash", Namespace.HASHES);
139                element.setAttribute(
140                        "algo",
141                        CaseFormat.UPPER_UNDERSCORE.to(
142                                CaseFormat.LOWER_HYPHEN, hash.algorithm.toString()));
143                element.setContent(BaseEncoding.base64().encode(hash.hash));
144            }
145            return checksum;
146        }
147    }
148
149    public static final class Received extends SessionInfo {
150
151        public Received(String name) {
152            super(name);
153        }
154
155        @Override
156        public Element asElement() {
157            final var element = new Element("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
158            element.setAttribute("name", name);
159            return element;
160        }
161    }
162
163    public abstract static sealed class SessionInfo permits Checksum, Received {
164
165        public final String name;
166
167        protected SessionInfo(final String name) {
168            this.name = name;
169        }
170
171        public abstract Element asElement();
172    }
173
174    public static class File {
175        public final long size;
176        public final String name;
177        public final String mediaType;
178
179        public final List<Hash> hashes;
180
181        public File(long size, String name, String mediaType, List<Hash> hashes) {
182            this.size = size;
183            this.name = name;
184            this.mediaType = mediaType;
185            this.hashes = hashes;
186        }
187
188        @Override
189        @NonNull
190        public String toString() {
191            return MoreObjects.toStringHelper(this)
192                    .add("size", size)
193                    .add("name", name)
194                    .add("mediaType", mediaType)
195                    .add("hashes", hashes)
196                    .toString();
197        }
198    }
199
200    public static class Hash {
201        public final byte[] hash;
202        public final Algorithm algorithm;
203
204        public Hash(byte[] hash, Algorithm algorithm) {
205            this.hash = hash;
206            this.algorithm = algorithm;
207        }
208
209        @Override
210        @NonNull
211        public String toString() {
212            return MoreObjects.toStringHelper(this)
213                    .add("hash", hash)
214                    .add("algorithm", algorithm)
215                    .toString();
216        }
217    }
218
219    public enum Algorithm {
220        SHA_1,
221        SHA_256;
222
223        public static Algorithm of(final String value) {
224            if (Strings.isNullOrEmpty(value)) {
225                return null;
226            }
227            return valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
228        }
229    }
230}