001package ezvcard.util;
002
003import java.io.UnsupportedEncodingException;
004import java.nio.charset.Charset;
005import java.util.Arrays;
006import java.util.Objects;
007
008import ezvcard.Messages;
009import ezvcard.util.org.apache.commons.codec.binary.Base64;
010
011/*
012 Copyright (c) 2012-2026, Michael Angstadt
013 All rights reserved.
014
015 Redistribution and use in source and binary forms, with or without
016 modification, are permitted provided that the following conditions are met: 
017
018 1. Redistributions of source code must retain the above copyright notice, this
019 list of conditions and the following disclaimer. 
020 2. Redistributions in binary form must reproduce the above copyright notice,
021 this list of conditions and the following disclaimer in the documentation
022 and/or other materials provided with the distribution. 
023
024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
027 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
028 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
029 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
030 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
031 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
032 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
033 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
034
035 The views and conclusions contained in the software and documentation are those
036 of the authors and should not be interpreted as representing official policies, 
037 either expressed or implied, of the FreeBSD Project.
038 */
039
040/**
041 * <p>
042 * Represents a data URI.
043 * </p>
044 * <p>
045 * Example: {@code data:image/jpeg;base64,[base64 string]}
046 * </p>
047 * @author Michael Angstadt
048 */
049public final class DataUri {
050        private static final String SCHEME = "data:";
051
052        private final byte[] data;
053        private final String text;
054        private final String contentType;
055
056        /**
057         * Creates a data URI.
058         * @param contentType the content type of the data (e.g. "image/png")
059         * @param data the data
060         */
061        public DataUri(String contentType, byte[] data) {
062                this(contentType, data, null);
063        }
064
065        /**
066         * Creates a data URI.
067         * @param contentType the content type of the text (e.g. "text/html")
068         * @param text the text
069         */
070        public DataUri(String contentType, String text) {
071                this(contentType, null, text);
072        }
073
074        /**
075         * Creates a data URI with a content type of "text/plain".
076         * @param text the text
077         */
078        public DataUri(String text) {
079                this("text/plain", text);
080        }
081
082        /**
083         * Copies a data URI.
084         * @param original the data URI to copy
085         */
086        public DataUri(DataUri original) {
087                this(original.contentType, (original.data == null) ? null : original.data.clone(), original.text);
088        }
089
090        private DataUri(String contentType, byte[] data, String text) {
091                this.contentType = (contentType == null) ? "" : contentType.toLowerCase();
092                this.data = data;
093                this.text = text;
094        }
095
096        /**
097         * Parses a data URI string.
098         * @param uri the URI string (e.g. "data:image/jpeg;base64,[base64 string]")
099         * @return the parsed data URI
100         * @throws IllegalArgumentException if the string is not a valid data URI or
101         * it cannot be parsed
102         */
103        public static DataUri parse(String uri) {
104                return new Parser(uri).parse();
105        }
106
107        private static class Parser {
108                private final String uri;
109                private final CharIterator it;
110
111                private String contentType;
112                private String charset;
113                private boolean base64;
114                private String dataStr;
115                private int tokenStart;
116
117                public Parser(String uri) {
118                        this.uri = uri;
119                        checkScheme();
120
121                        tokenStart = SCHEME.length();
122                        it = new CharIterator(uri, tokenStart);
123                }
124
125                private void checkScheme() {
126                        if (uri.length() < SCHEME.length() || !uri.substring(0, SCHEME.length()).equalsIgnoreCase(SCHEME)) {
127                                //not a data URI
128                                throw Messages.INSTANCE.getIllegalArgumentException(18, SCHEME);
129                        }
130                }
131
132                public DataUri parse() {
133                        //Syntax: data:[<media type>][;charset=<character set>][;base64],<data>
134                        while (it.hasNext()) {
135                                char c = it.next();
136
137                                if (c == ',') {
138                                        handleComma();
139                                        break;
140                                }
141
142                                if (c == ';') {
143                                        handleSemicolon();
144                                }
145                        }
146
147                        if (dataStr == null) {
148                                throw Messages.INSTANCE.getIllegalArgumentException(20);
149                        }
150
151                        return build();
152                }
153
154                private void handleComma() {
155                        String token = uri.substring(tokenStart, it.index());
156                        if (contentType == null) {
157                                contentType = token.toLowerCase();
158                        } else {
159                                if (token.toLowerCase().startsWith("charset=")) {
160                                        int equals = token.indexOf('=');
161                                        charset = token.substring(equals + 1);
162                                } else if ("base64".equalsIgnoreCase(token)) {
163                                        base64 = true;
164                                }
165                        }
166
167                        dataStr = uri.substring(it.index() + 1);
168                }
169
170                private void handleSemicolon() {
171                        String token = uri.substring(tokenStart, it.index());
172                        if (contentType == null) {
173                                contentType = token.toLowerCase();
174                        } else {
175                                if (token.toLowerCase().startsWith("charset=")) {
176                                        int equals = token.indexOf('=');
177                                        charset = token.substring(equals + 1);
178                                } else if ("base64".equalsIgnoreCase(token)) {
179                                        base64 = true;
180                                }
181                        }
182
183                        tokenStart = it.index() + 1;
184                }
185
186                private DataUri build() {
187                        String text = null;
188                        byte[] data = null;
189
190                        if (base64) {
191                                dataStr = dataStr.replaceAll("\\s", "");
192                                data = Base64.decodeBase64(dataStr);
193                                if (charset != null) {
194                                        try {
195                                                text = new String(data, charset);
196                                        } catch (UnsupportedEncodingException e) {
197                                                throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(43, charset), e);
198                                        }
199                                        data = null;
200                                }
201                        } else {
202                                text = dataStr;
203                        }
204
205                        return new DataUri(contentType, data, text);
206                }
207        }
208
209        /**
210         * Gets the binary data.
211         * @return the binary data or null if the value was text
212         */
213        public byte[] getData() {
214                return data;
215        }
216
217        /**
218         * Gets the content type.
219         * @return the content type (e.g. "image/png")
220         */
221        public String getContentType() {
222                return contentType;
223        }
224
225        /**
226         * Gets the text value.
227         * @return the text value or null if the value was binary
228         */
229        public String getText() {
230                return text;
231        }
232
233        /**
234         * Creates a data URI string.
235         * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]")
236         */
237        @Override
238        public String toString() {
239                return toString(null);
240        }
241
242        /**
243         * Creates a data URI string.
244         * @param charset only applicable if the data URI's value is text. Defines
245         * what character set to use when encoding text value in base64. Setting
246         * this to "null" will insert the text in the data URI as-is without
247         * base64-encoding it.
248         * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]")
249         */
250        public String toString(Charset charset) {
251                StringBuilder sb = new StringBuilder();
252                sb.append(SCHEME);
253                sb.append(contentType);
254
255                if (data != null) {
256                        sb.append(";base64,");
257                        sb.append(Base64.encodeBase64String(data));
258                } else if (text != null) {
259                        if (charset == null) {
260                                sb.append(',').append(text);
261                        } else {
262                                byte[] textBytes = text.getBytes(charset);
263                                sb.append(";charset=").append(charset.name());
264                                sb.append(";base64,");
265                                sb.append(Base64.encodeBase64String(textBytes));
266                        }
267                } else {
268                        sb.append(',');
269                }
270
271                return sb.toString();
272        }
273
274        @Override
275        public int hashCode() {
276                final int prime = 31;
277                int result = 1;
278                result = prime * result + Arrays.hashCode(data);
279                result = prime * result + Objects.hash(contentType, text);
280                return result;
281        }
282
283        @Override
284        public boolean equals(Object obj) {
285                if (this == obj) return true;
286                if (obj == null) return false;
287                if (getClass() != obj.getClass()) return false;
288                DataUri other = (DataUri) obj;
289                return Objects.equals(contentType, other.contentType) && Arrays.equals(data, other.data) && Objects.equals(text, other.text);
290        }
291}