diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java index 171d266c..4da78f68 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -28,11 +28,8 @@ public class AttachmentUtils { public static SignalServiceAttachmentStream createAttachmentStream(String attachment) throws AttachmentInvalidException { try { final var streamDetails = Utils.createStreamDetails(attachment); - final var name = streamDetails.getStream() instanceof FileInputStream - ? new File(attachment).getName() - : null; - return createAttachmentStream(streamDetails, Optional.ofNullable(name)); + return createAttachmentStream(streamDetails.first(), streamDetails.second()); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java b/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java new file mode 100644 index 00000000..1e2e0038 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/util/DataURI.java @@ -0,0 +1,64 @@ +package org.asamk.signal.manager.util; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Data URI record that follows RFC 2397. + * + * @param mediaType the media type. If empty, the default media type "text/plain" is used. + * @param parameter the list of parameters. Must be URL escaped encoded. + * @param data the data. If base64 is not given, the data is treated as ASCII string. + */ +@SuppressWarnings({"java:S6218"}) +public record DataURI(String mediaType, Map parameter, byte[] data) { + + public static final Pattern DATA_URI_PATTERN = Pattern.compile( + "data:(?.+?\\/.+?)?(?;.+?)?(?;base64)?,(?.+)", + Pattern.CASE_INSENSITIVE); + public static final Pattern PARAMETER_PATTERN = Pattern.compile("\\G;(?.+)=(?.+)", + Pattern.CASE_INSENSITIVE); + public static final String DEFAULT_TYPE = "text/plain"; + + /** + * Generates a new {@link DataURI} object from the given string. + * + * @param dataURI the data URI + * @return a data URI object + * @throws IllegalArgumentException if the given string is not a valid data URI + */ + public static DataURI of(final String dataURI) { + final var matcher = DATA_URI_PATTERN.matcher(dataURI); + + if (!matcher.find()) { + throw new IllegalArgumentException("The given string is not a valid data URI."); + } + + final Map parameters = new HashMap<>(); + final var params = matcher.group("parameters"); + if (params != null) { + final Matcher paramsMatcher = PARAMETER_PATTERN.matcher(params); + while (paramsMatcher.find()) { + final var key = paramsMatcher.group("key"); + final var value = URLDecoder.decode(paramsMatcher.group("value"), StandardCharsets.UTF_8); + parameters.put(key, value); + } + } + + final boolean isBase64 = matcher.group("base64") != null; + final byte[] data; + if (isBase64) { + data = Base64.getDecoder().decode(matcher.group("data").getBytes(StandardCharsets.UTF_8)); + } else { + data = URLDecoder.decode(matcher.group("data"), StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8); + } + + return new DataURI(Optional.ofNullable(matcher.group("type")).orElse(DEFAULT_TYPE), parameters, data); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index 0e815d10..e71e6412 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager.util; +import org.asamk.signal.manager.api.Pair; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; @@ -22,6 +23,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; import java.util.function.BiFunction; @@ -33,10 +35,10 @@ public class Utils { private final static Logger logger = LoggerFactory.getLogger(Utils.class); - public static String getFileMimeType(File file, String defaultMimeType) throws IOException { + public static String getFileMimeType(final File file, final String defaultMimeType) throws IOException { var mime = Files.probeContentType(file.toPath()); if (mime == null) { - try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { + try (final InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { mime = URLConnection.guessContentTypeFromStream(bufferedStream); } } @@ -46,42 +48,29 @@ public class Utils { return mime; } - private static boolean isBase64DataString(final String[] parts) { - return parts.length == 2 - && parts[0].startsWith("data:") - && parts[0].contains("/") - && parts[1].startsWith("base64,"); + public static Pair> createStreamDetailsFromDataURI(final String dataURI) { + final DataURI uri = DataURI.of(dataURI); + + return new Pair<>(new StreamDetails( + new ByteArrayInputStream(uri.data()), uri.mediaType(), uri.data().length), + Optional.ofNullable(uri.parameter().get("filename"))); } - public static boolean isBase64DataString(final String value) { - return isBase64DataString(value.split(";", 2)); - } - - public static StreamDetails createStreamDetailsFromBase64(final String base64) { - final String[] parts = base64.split(";", 2); - if (!isBase64DataString(parts)) { - throw new IllegalArgumentException("The given argument is not a valid base64 string."); - } - - parts[0] = parts[0].substring(5); - final byte[] bytes = Base64.getDecoder().decode(parts[1].substring(7).getBytes(StandardCharsets.UTF_8)); - - return new StreamDetails(new ByteArrayInputStream(bytes), parts[0], bytes.length); - } - - public static StreamDetails createStreamDetailsFromFile(File file) throws IOException { - InputStream stream = new FileInputStream(file); + public static StreamDetails createStreamDetailsFromFile(final File file) throws IOException { + final InputStream stream = new FileInputStream(file); final var size = file.length(); final var mime = getFileMimeType(file, "application/octet-stream"); return new StreamDetails(stream, mime, size); } - public static StreamDetails createStreamDetails(final String value) throws IOException { - if (isBase64DataString(value)) { - return createStreamDetailsFromBase64(value); - } + public static Pair> createStreamDetails(final String value) throws IOException { + try { + return createStreamDetailsFromDataURI(value); + } catch (final IllegalArgumentException e) { + final File f = new File(value); - return createStreamDetailsFromFile(new File(value)); + return new Pair<>(createStreamDetailsFromFile(f), Optional.of(f.getName())); + } } public static Fingerprint computeSafetyNumber( diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 7ffd9fc7..5c05e787 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -56,8 +56,8 @@ public class SendCommand implements JsonRpcLocalCommand { .action(Arguments.storeTrue()) .help("Read the message from standard input."); subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment." - + "Base64 encoded attachments can be added and must follow the format " - + "data:;base64,."); + + "Data URI encoded attachments can be added and must follow the RFC 2397. Additionally a file name can be added, e.g. " + + "data:;filename=;base64,."); subparser.addArgument("-e", "--end-session", "--endsession") .help("Clear session state and send end session message.") .action(Arguments.storeTrue());