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}