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}