001/*
002 * Copyright 2007-2021 The jdeb developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.vafer.jdeb.utils;
017
018import java.io.ByteArrayOutputStream;
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.io.OutputStream;
025import java.text.SimpleDateFormat;
026import java.util.Collection;
027import java.util.Date;
028import java.util.Iterator;
029import java.util.LinkedHashSet;
030import java.util.Map;
031import java.util.TimeZone;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034
035import org.apache.tools.ant.filters.FixCrLfFilter;
036import org.apache.tools.ant.util.ReaderInputStream;
037
038/**
039 * Simple utils functions.
040 *
041 * ATTENTION: don't use outside of jdeb
042 */
043public final class Utils {
044    private static final Pattern BETA_PATTERN = Pattern.compile("^(?:(?:(.*?)([.\\-_]))|(.*[^a-z]))(alpha|a|beta|b|milestone|m|cr|rc)([^a-z].*)?$", Pattern.CASE_INSENSITIVE);
045
046    private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("(.*)[\\-+]SNAPSHOT");
047
048    public static int copy( final InputStream pInput, final OutputStream pOutput ) throws IOException {
049        final byte[] buffer = new byte[2048];
050        int count = 0;
051        int n;
052        while (-1 != (n = pInput.read(buffer))) {
053            pOutput.write(buffer, 0, n);
054            count += n;
055        }
056        return count;
057    }
058
059    public static String toHex( final byte[] bytes ) {
060        final StringBuilder sb = new StringBuilder();
061
062        for (byte b : bytes) {
063            sb.append(Integer.toHexString((b >> 4) & 0x0f));
064            sb.append(Integer.toHexString(b & 0x0f));
065        }
066
067        return sb.toString();
068    }
069
070    public static String stripPath( final int p, final String s ) {
071
072        if (p <= 0) {
073            return s;
074        }
075
076        int x = 0;
077        for (int i = 0; i < p; i++) {
078            x = s.indexOf('/', x + 1);
079            if (x < 0) {
080                return s;
081            }
082        }
083
084        return s.substring(x + 1);
085    }
086
087    private static String joinPath(char sep, String ...paths) {
088        final StringBuilder sb = new StringBuilder();
089        for (String p : paths) {
090            if (p == null) continue;
091            if (!p.startsWith("/") && sb.length() > 0) {
092                sb.append(sep);
093            }
094            sb.append(p);
095        }
096        return sb.toString();
097    }
098
099    public static String joinUnixPath(String ...paths) {
100        return joinPath('/', paths);
101    }
102
103    public static String joinLocalPath(String ...paths) {
104        return joinPath(File.separatorChar, paths);
105    }
106
107    public static String stripLeadingSlash( final String s ) {
108        if (s == null) {
109            return s;
110        }
111        if (s.length() == 0) {
112            return s;
113        }
114        if (s.charAt(0) == '/' || s.charAt(0) == '\\') {
115            return s.substring(1);
116        }
117        return s;
118    }
119
120    /**
121     * Substitute the variables in the given expression with the
122     * values from the resolver
123     *
124     * @param pResolver
125     * @param pExpression
126     */
127    public static String replaceVariables( final VariableResolver pResolver, final String pExpression, final String pOpen, final String pClose ) {
128        final char[] open = pOpen.toCharArray();
129        final char[] close = pClose.toCharArray();
130
131        final StringBuilder out = new StringBuilder();
132        StringBuilder sb = new StringBuilder();
133        char[] last = null;
134        int wo = 0;
135        int wc = 0;
136        int level = 0;
137        for (char c : pExpression.toCharArray()) {
138            if (c == open[wo]) {
139                if (wc > 0) {
140                    sb.append(close, 0, wc);
141                }
142                wc = 0;
143                wo++;
144                if (open.length == wo) {
145                    // found open
146                    if (last == open) {
147                        out.append(open);
148                    }
149                    level++;
150                    out.append(sb);
151                    sb = new StringBuilder();
152                    wo = 0;
153                    last = open;
154                }
155            } else if (c == close[wc]) {
156                if (wo > 0) {
157                    sb.append(open, 0, wo);
158                }
159                wo = 0;
160                wc++;
161                if (close.length == wc) {
162                    // found close
163                    if (last == open) {
164                        final String variable = pResolver.get(sb.toString());
165                        if (variable != null) {
166                            out.append(variable);
167                        } else {
168                            out.append(open);
169                            out.append(sb);
170                            out.append(close);
171                        }
172                    } else {
173                        out.append(sb);
174                        out.append(close);
175                    }
176                    sb = new StringBuilder();
177                    level--;
178                    wc = 0;
179                    last = close;
180                }
181            } else {
182
183                if (wo > 0) {
184                    sb.append(open, 0, wo);
185                }
186
187                if (wc > 0) {
188                    sb.append(close, 0, wc);
189                }
190
191                sb.append(c);
192
193                wo = wc = 0;
194            }
195        }
196
197        if (wo > 0) {
198            sb.append(open, 0, wo);
199        }
200
201        if (wc > 0) {
202            sb.append(close, 0, wc);
203        }
204
205        if (level > 0) {
206            out.append(open);
207        }
208        out.append(sb);
209
210        return out.toString();
211    }
212
213    /**
214     * Replaces new line delimiters in the input stream with the Unix line feed.
215     *
216     * @param input
217     */
218    public static byte[] toUnixLineEndings( InputStream input ) throws IOException {
219        String encoding = "ISO-8859-1";
220        FixCrLfFilter filter = new FixCrLfFilter(new InputStreamReader(input, encoding));
221        filter.setEol(FixCrLfFilter.CrLf.newInstance("unix"));
222
223        ByteArrayOutputStream filteredFile = new ByteArrayOutputStream();
224        Utils.copy(new ReaderInputStream(filter, encoding), filteredFile);
225
226        return filteredFile.toByteArray();
227    }
228
229    private static String formatSnapshotTemplate( String template, Date timestamp ) {
230        int startBracket = template.indexOf('[');
231        int endBracket = template.indexOf(']');
232        if(startBracket == -1 || endBracket == -1) {
233            return template;
234        } else {
235            // prefix[yyMMdd]suffix
236            final String date = new SimpleDateFormat(template.substring(startBracket + 1, endBracket)).format(timestamp);
237            String datePrefix = startBracket == 0 ? "" : template.substring(0, startBracket);
238            String dateSuffix = endBracket == template.length() ? "" : template.substring(endBracket + 1);
239            return datePrefix + date + dateSuffix;
240        }
241    }
242
243    /**
244     * Convert the project version to a version suitable for a Debian package.
245     * -SNAPSHOT suffixes are replaced with a timestamp (~yyyyMMddHHmmss).
246     * The separator before a rc, alpha or beta version is replaced with '~'
247     * such that the version is always ordered before the final or GA release.
248     *
249     * @param version the project version to convert to a Debian package version
250     * @param template the template used to replace -SNAPSHOT, the timestamp format is in brackets,
251     *        the rest of the string is preserved (prefix[yyMMdd]suffix -> prefix151230suffix)
252     * @param timestamp the UTC date used as the timestamp to replace the SNAPSHOT suffix.
253     */
254    public static String convertToDebianVersion( String version, boolean apply, String envName, String template, Date timestamp ) {
255        Matcher matcher = SNAPSHOT_PATTERN.matcher(version);
256        if (matcher.matches()) {
257            version = matcher.group(1) + "~";
258
259            if (apply) {
260                final String envValue = System.getenv(envName);
261                if(template != null && template.length() > 0) {
262                    version += formatSnapshotTemplate(template, timestamp);
263                } else if (envValue != null && envValue.length() > 0) {
264                    version += envValue;
265                } else {
266                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
267                    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
268                    version += dateFormat.format(timestamp);
269                }
270            } else {
271                version += "SNAPSHOT";
272            }
273        } else {
274            matcher = BETA_PATTERN.matcher(version);
275            if (matcher.matches()) {
276                if (matcher.group(1) != null) {
277                    version = matcher.group(1) + "~" + matcher.group(4) + matcher.group(5);
278                } else {
279                    version = matcher.group(3) + "~" + matcher.group(4) + matcher.group(5);
280                }
281            }
282        }
283
284        // safest upstream_version should only contain full stop, plus, tilde, and alphanumerics
285        // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
286        version = version.replaceAll("[^.+~A-Za-z0-9]", "+").replaceAll("\\++", "+");
287
288        return version;
289    }
290
291    /**
292     * Construct new path by replacing file directory part. No
293     * files are actually modified.
294     * @param file path to move
295     * @param target new path directory
296     */
297    public static String movePath( final String file,
298                                   final String target ) {
299        final String name = new File(file).getName();
300        return target.endsWith("/") ? target + name : target + '/' + name;
301    }
302
303    /**
304     * Extracts value from map if given value is null.
305     * @param value current value
306     * @param props properties to extract value from
307     * @param key property name to extract
308     * @return initial value or value extracted from map
309     */
310    public static String lookupIfEmpty( final String value,
311                                        final Map<String, String> props,
312                                        final String key ) {
313        return value != null ? value : props.get(key);
314    }
315
316    /**
317    * Get the known locations where the secure keyring can be located.
318    * Looks through known locations of the GNU PG secure keyring.
319    *
320    * @return The location of the PGP secure keyring if it was found,
321    *         null otherwise
322    */
323    public static Collection<String> getKnownPGPSecureRingLocations() {
324        final LinkedHashSet<String> locations = new LinkedHashSet<>();
325
326        final String os = System.getProperty("os.name");
327        final boolean runOnWindows = os == null || os.toLowerCase().contains("win");
328
329        if (runOnWindows) {
330            // The user's roaming profile on Windows, via environment
331            final String windowsRoaming = System.getenv("APPDATA");
332            if (windowsRoaming != null) {
333                locations.add(joinLocalPath(windowsRoaming, "gnupg", "secring.gpg"));
334            }
335
336            // The user's local profile on Windows, via environment
337            final String windowsLocal = System.getenv("LOCALAPPDATA");
338            if (windowsLocal != null) {
339                locations.add(joinLocalPath(windowsLocal, "gnupg", "secring.gpg"));
340            }
341
342            // The Windows installation directory
343            final String windir = System.getProperty("WINDIR");
344            if (windir != null) {
345                // Local Profile on Windows 98 and ME
346                locations.add(joinLocalPath(windir, "Application Data", "gnupg", "secring.gpg"));
347            }
348        }
349
350        final String home = System.getProperty("user.home");
351
352        if (home != null && runOnWindows) {
353            // These are for various flavours of Windows
354            // if the environment variables above have failed
355
356            // Roaming profile on Vista and later
357            locations.add(joinLocalPath(home, "AppData", "Roaming", "gnupg", "secring.gpg"));
358            // Local profile on Vista and later
359            locations.add(joinLocalPath(home, "AppData", "Local", "gnupg", "secring.gpg"));
360            // Roaming profile on 2000 and XP
361            locations.add(joinLocalPath(home, "Application Data", "gnupg", "secring.gpg"));
362            // Local profile on 2000 and XP
363            locations.add(joinLocalPath(home, "Local Settings", "Application Data", "gnupg", "secring.gpg"));
364        }
365
366        // *nix, including OS X
367        if (home != null) {
368            locations.add(joinLocalPath(home, ".gnupg", "secring.gpg"));
369        }
370
371        return locations;
372    }
373
374    /**
375     * Tries to guess location of the user secure keyring using various
376     * heuristics.
377     *
378     * @return path to the keyring file
379     * @throws FileNotFoundException if no keyring file found
380     */
381    public static File guessKeyRingFile() throws FileNotFoundException {
382        final Collection<String> possibleLocations = getKnownPGPSecureRingLocations();
383        for (final String location : possibleLocations) {
384            final File candidate = new File(location);
385            if (candidate.exists()) {
386                return candidate;
387            }
388        }
389        final StringBuilder message = new StringBuilder("Could not locate secure keyring, locations tried: ");
390        final Iterator<String> it = possibleLocations.iterator();
391        while (it.hasNext()) {
392            message.append(it.next());
393            if (it.hasNext()) {
394                message.append(", ");
395            }
396        }
397        throw new FileNotFoundException(message.toString());
398    }
399
400    /**
401     * Returns true if string is null or empty.
402     */
403    public static boolean isNullOrEmpty(final String str) {
404        return str == null || str.length() == 0;
405    }
406
407    /**
408    * Return fallback if first string is null or empty
409    */
410    public static String defaultString(final String str, final String fallback) {
411        return isNullOrEmpty(str) ? fallback : str;
412    }
413
414
415    /**
416     * Check if a CharSequence is whitespace, empty ("") or null.
417     *
418     * <pre>
419     * StringUtils.isBlank(null)      = true
420     * StringUtils.isBlank("")        = true
421     * StringUtils.isBlank(" ")       = true
422     * StringUtils.isBlank("bob")     = false
423     * StringUtils.isBlank("  bob  ") = false
424     * </pre>
425     *
426     * @param cs
427     *            the CharSequence to check, may be null
428     * @return {@code true} if the CharSequence is null, empty or whitespace
429     */
430    public static boolean isBlank(final CharSequence cs) {
431        int strLen;
432        if (cs == null || (strLen = cs.length()) == 0) {
433            return true;
434        }
435        for (int i = 0; i < strLen; i++) {
436            if (!Character.isWhitespace(cs.charAt(i))) {
437                return false;
438            }
439        }
440        return true;
441    }
442}