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}