001/*
002 * Copyright 2007-2018 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("/")) {
092                sb.append(p);
093            } else {
094                sb.append(sep);
095                sb.append(p);
096            }
097        }
098        return sb.toString();
099    }
100
101    public static String joinUnixPath(String ...paths) {
102        return joinPath('/', paths);
103    }
104
105    public static String joinLocalPath(String ...paths) {
106        return joinPath(File.separatorChar, paths);
107    }
108
109    public static String stripLeadingSlash( final String s ) {
110        if (s == null) {
111            return s;
112        }
113        if (s.length() == 0) {
114            return s;
115        }
116        if (s.charAt(0) == '/' || s.charAt(0) == '\\') {
117            return s.substring(1);
118        }
119        return s;
120    }
121
122    /**
123     * Substitute the variables in the given expression with the
124     * values from the resolver
125     *
126     * @param pResolver
127     * @param pExpression
128     */
129    public static String replaceVariables( final VariableResolver pResolver, final String pExpression, final String pOpen, final String pClose ) {
130        final char[] open = pOpen.toCharArray();
131        final char[] close = pClose.toCharArray();
132
133        final StringBuilder out = new StringBuilder();
134        StringBuilder sb = new StringBuilder();
135        char[] last = null;
136        int wo = 0;
137        int wc = 0;
138        int level = 0;
139        for (char c : pExpression.toCharArray()) {
140            if (c == open[wo]) {
141                if (wc > 0) {
142                    sb.append(close, 0, wc);
143                }
144                wc = 0;
145                wo++;
146                if (open.length == wo) {
147                    // found open
148                    if (last == open) {
149                        out.append(open);
150                    }
151                    level++;
152                    out.append(sb);
153                    sb = new StringBuilder();
154                    wo = 0;
155                    last = open;
156                }
157            } else if (c == close[wc]) {
158                if (wo > 0) {
159                    sb.append(open, 0, wo);
160                }
161                wo = 0;
162                wc++;
163                if (close.length == wc) {
164                    // found close
165                    if (last == open) {
166                        final String variable = pResolver.get(sb.toString());
167                        if (variable != null) {
168                            out.append(variable);
169                        } else {
170                            out.append(open);
171                            out.append(sb);
172                            out.append(close);
173                        }
174                    } else {
175                        out.append(sb);
176                        out.append(close);
177                    }
178                    sb = new StringBuilder();
179                    level--;
180                    wc = 0;
181                    last = close;
182                }
183            } else {
184
185                if (wo > 0) {
186                    sb.append(open, 0, wo);
187                }
188
189                if (wc > 0) {
190                    sb.append(close, 0, wc);
191                }
192
193                sb.append(c);
194
195                wo = wc = 0;
196            }
197        }
198
199        if (wo > 0) {
200            sb.append(open, 0, wo);
201        }
202
203        if (wc > 0) {
204            sb.append(close, 0, wc);
205        }
206
207        if (level > 0) {
208            out.append(open);
209        }
210        out.append(sb);
211
212        return out.toString();
213    }
214
215    /**
216     * Replaces new line delimiters in the input stream with the Unix line feed.
217     *
218     * @param input
219     */
220    public static byte[] toUnixLineEndings( InputStream input ) throws IOException {
221        String encoding = "ISO-8859-1";
222        FixCrLfFilter filter = new FixCrLfFilter(new InputStreamReader(input, encoding));
223        filter.setEol(FixCrLfFilter.CrLf.newInstance("unix"));
224
225        ByteArrayOutputStream filteredFile = new ByteArrayOutputStream();
226        Utils.copy(new ReaderInputStream(filter, encoding), filteredFile);
227
228        return filteredFile.toByteArray();
229    }
230
231    private static String formatSnapshotTemplate( String template, Date timestamp ) {
232        int startBracket = template.indexOf('[');
233        int endBracket = template.indexOf(']');
234        if(startBracket == -1 || endBracket == -1) {
235            return template;
236        } else {
237            // prefix[yyMMdd]suffix
238            final String date = new SimpleDateFormat(template.substring(startBracket + 1, endBracket)).format(timestamp);
239            String datePrefix = startBracket == 0 ? "" : template.substring(0, startBracket);
240            String dateSuffix = endBracket == template.length() ? "" : template.substring(endBracket + 1);
241            return datePrefix + date + dateSuffix;
242        }
243    }
244
245    /**
246     * Convert the project version to a version suitable for a Debian package.
247     * -SNAPSHOT suffixes are replaced with a timestamp (~yyyyMMddHHmmss).
248     * The separator before a rc, alpha or beta version is replaced with '~'
249     * such that the version is always ordered before the final or GA release.
250     *
251     * @param version the project version to convert to a Debian package version
252     * @param template the template used to replace -SNAPSHOT, the timestamp format is in brackets,
253     *        the rest of the string is preserved (prefix[yyMMdd]suffix -> prefix151230suffix)
254     * @param timestamp the UTC date used as the timestamp to replace the SNAPSHOT suffix.
255     */
256    public static String convertToDebianVersion( String version, boolean apply, String envName, String template, Date timestamp ) {
257        Matcher matcher = SNAPSHOT_PATTERN.matcher(version);
258        if (matcher.matches()) {
259            version = matcher.group(1) + "~";
260
261            if (apply) {
262                final String envValue = System.getenv(envName);
263                if(template != null && template.length() > 0) {
264                    version += formatSnapshotTemplate(template, timestamp);
265                } else if (envValue != null && envValue.length() > 0) {
266                    version += envValue;
267                } else {
268                    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
269                    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
270                    version += dateFormat.format(timestamp);
271                }
272            } else {
273                version += "SNAPSHOT";
274            }
275        } else {
276            matcher = BETA_PATTERN.matcher(version);
277            if (matcher.matches()) {
278                if (matcher.group(1) != null) {
279                    version = matcher.group(1) + "~" + matcher.group(4) + matcher.group(5);
280                } else {
281                    version = matcher.group(3) + "~" + matcher.group(4) + matcher.group(5);
282                }
283            }
284        }
285
286        // safest upstream_version should only contain full stop, plus, tilde, and alphanumerics
287        // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
288        version = version.replaceAll("[^\\.+~A-Za-z0-9]", "+").replaceAll("\\++", "+");
289
290        return version;
291    }
292
293    /**
294     * Construct new path by replacing file directory part. No
295     * files are actually modified.
296     * @param file path to move
297     * @param target new path directory
298     */
299    public static String movePath( final String file,
300                                   final String target ) {
301        final String name = new File(file).getName();
302        return target.endsWith("/") ? target + name : target + '/' + name;
303    }
304
305    /**
306     * Extracts value from map if given value is null.
307     * @param value current value
308     * @param props properties to extract value from
309     * @param key property name to extract
310     * @return initial value or value extracted from map
311     */
312    public static String lookupIfEmpty( final String value,
313                                        final Map<String, String> props,
314                                        final String key ) {
315        return value != null ? value : props.get(key);
316    }
317
318    /**
319    * Get the known locations where the secure keyring can be located.
320    * Looks through known locations of the GNU PG secure keyring.
321    *
322    * @return The location of the PGP secure keyring if it was found,
323    *         null otherwise
324    */
325    public static Collection<String> getKnownPGPSecureRingLocations() {
326        final LinkedHashSet<String> locations = new LinkedHashSet<String>();
327
328        final String os = System.getProperty("os.name");
329        final boolean runOnWindows = os == null || os.toLowerCase().contains("win");
330
331        if (runOnWindows) {
332            // The user's roaming profile on Windows, via environment
333            final String windowsRoaming = System.getenv("APPDATA");
334            if (windowsRoaming != null) {
335                locations.add(joinLocalPath(windowsRoaming, "gnupg", "secring.gpg"));
336            }
337
338            // The user's local profile on Windows, via environment
339            final String windowsLocal = System.getenv("LOCALAPPDATA");
340            if (windowsLocal != null) {
341                locations.add(joinLocalPath(windowsLocal, "gnupg", "secring.gpg"));
342            }
343
344            // The Windows installation directory
345            final String windir = System.getProperty("WINDIR");
346            if (windir != null) {
347                // Local Profile on Windows 98 and ME
348                locations.add(joinLocalPath(windir, "Application Data", "gnupg", "secring.gpg"));
349            }
350        }
351
352        final String home = System.getProperty("user.home");
353
354        if (home != null && runOnWindows) {
355            // These are for various flavours of Windows
356            // if the environment variables above have failed
357
358            // Roaming profile on Vista and later
359            locations.add(joinLocalPath(home, "AppData", "Roaming", "gnupg", "secring.gpg"));
360            // Local profile on Vista and later
361            locations.add(joinLocalPath(home, "AppData", "Local", "gnupg", "secring.gpg"));
362            // Roaming profile on 2000 and XP
363            locations.add(joinLocalPath(home, "Application Data", "gnupg", "secring.gpg"));
364            // Local profile on 2000 and XP
365            locations.add(joinLocalPath(home, "Local Settings", "Application Data", "gnupg", "secring.gpg"));
366        }
367
368        // *nix, including OS X
369        if (home != null) {
370            locations.add(joinLocalPath(home, ".gnupg", "secring.gpg"));
371        }
372
373        return locations;
374    }
375
376    /**
377     * Tries to guess location of the user secure keyring using various
378     * heuristics.
379     *
380     * @return path to the keyring file
381     * @throws FileNotFoundException if no keyring file found
382     */
383    public static File guessKeyRingFile() throws FileNotFoundException {
384        final Collection<String> possibleLocations = getKnownPGPSecureRingLocations();
385        for (final String location : possibleLocations) {
386            final File candidate = new File(location);
387            if (candidate.exists()) {
388                return candidate;
389            }
390        }
391        final StringBuilder message = new StringBuilder("Could not locate secure keyring, locations tried: ");
392        final Iterator<String> it = possibleLocations.iterator();
393        while (it.hasNext()) {
394            message.append(it.next());
395            if (it.hasNext()) {
396                message.append(", ");
397            }
398        }
399        throw new FileNotFoundException(message.toString());
400    }
401
402    /**
403     * Returns true if string is null or empty.
404     */
405    public static boolean isNullOrEmpty(final String str) {
406        return str == null || str.length() == 0;
407    }
408
409    /**
410    * Return fallback if first string is null or empty
411    */
412    public static String defaultString(final String str, final String fallback) {
413        return isNullOrEmpty(str) ? fallback : str;
414    }
415
416
417    /**
418     * Check if a CharSequence is whitespace, empty ("") or null.
419     *
420     * <pre>
421     * StringUtils.isBlank(null)      = true
422     * StringUtils.isBlank("")        = true
423     * StringUtils.isBlank(" ")       = true
424     * StringUtils.isBlank("bob")     = false
425     * StringUtils.isBlank("  bob  ") = false
426     * </pre>
427     *
428     * @param cs
429     *            the CharSequence to check, may be null
430     * @return {@code true} if the CharSequence is null, empty or whitespace
431     */
432    public static boolean isBlank(final CharSequence cs) {
433        int strLen;
434        if (cs == null || (strLen = cs.length()) == 0) {
435            return true;
436        }
437        for (int i = 0; i < strLen; i++) {
438            if (Character.isWhitespace(cs.charAt(i)) == false) {
439                return false;
440            }
441        }
442        return true;
443    }
444}