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